aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorJay <jeremy.davis@sonarsource.com>2023-07-06 17:32:55 +0200
committersonartech <sonartech@sonarsource.com>2023-07-11 20:03:24 +0000
commit76c1f065c934a9abcba93dedb97fe7b4b8277ec4 (patch)
treef0e190f826d1736b02b0eba4a2488c0e649614f8 /server/sonar-web/src/main
parentf062b56fd8656ee6239335a561474e25f73af5f5 (diff)
downloadsonarqube-76c1f065c934a9abcba93dedb97fe7b4b8277ec4.tar.gz
sonarqube-76c1f065c934a9abcba93dedb97fe7b4b8277ec4.zip
SONAR-19709 Migrate projects facets to the new UI
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ClearAll.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx108
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ClearAll-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ClearAll-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.tsx39
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx225
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx139
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/NewCoverageFilter.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/NewDuplicationsFilter.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualifierFilter.tsx83
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx165
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx (renamed from server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx)44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.tsx60
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.tsx111
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/DuplicationsFilter-test.tsx39
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx138
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/FilterHeader-test.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/MaintainabilityFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewCoverageFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewDuplicationsFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewLinesFilter-test.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewMaintainabilityFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewReliabilityFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewSecurityFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualifierFilter-test.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/ReliabilityFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.tsx55
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterOption-test.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityFilter-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityReviewFilter-test.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/SizeFilter-test.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/DuplicationsFilter-test.tsx.snap61
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/Filter-test.tsx.snap580
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/FilterHeader-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/IssuesFilter-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.tsx.snap264
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/MaintainabilityFilter-test.tsx.snap21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewCoverageFilter-test.tsx.snap9
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewDuplicationsFilter-test.tsx.snap9
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewMaintainabilityFilter-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewReliabilityFilter-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewSecurityFilter-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualifierFilter-test.tsx.snap34
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualityGateFilter-test.tsx.snap31
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/ReliabilityFilter-test.tsx.snap21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterOption-test.tsx.snap13
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityFilter-test.tsx.snap21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityReviewFilter-test.tsx.snap50
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SizeFilter-test.tsx.snap42
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.tsx.snap365
-rw-r--r--server/sonar-web/src/main/js/apps/projects/utils.ts4
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/ratings-test.ts13
-rw-r--r--server/sonar-web/src/main/js/helpers/ratings.ts11
80 files changed, 887 insertions, 3460 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx
index dce39a06677..106ea557690 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx
@@ -19,12 +19,13 @@
*/
import { FacetBox, FacetItem, FlagMessage, InputSearch } from 'design-system';
-import { sortBy, without } from 'lodash';
+import { max, sortBy, values, without } from 'lodash';
import * as React from 'react';
import ListFooter from '../../../components/controls/ListFooter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { queriesEqual } from '../../../helpers/query';
+import { isDefined } from '../../../helpers/types';
import { MetricType } from '../../../types/metrics';
import { Dict, Paging, RawQuery } from '../../../types/types';
import { FacetItemsList } from './FacetItemsList';
@@ -39,6 +40,7 @@ interface SearchResponse<S> {
export interface Props<S> {
disabled?: boolean;
+ disableZero?: boolean;
facetHeader: string;
fetching: boolean;
getFacetItemText: (item: string) => string;
@@ -54,7 +56,7 @@ export interface Props<S> {
onClear?: () => void;
onItemClick?: (itemValue: string, multiple: boolean) => void;
onSearch: (query: string, page?: number) => Promise<SearchResponse<S>>;
- onToggle: (property: string) => void;
+ onToggle?: (property: string) => void;
open: boolean;
property: string;
query?: RawQuery;
@@ -63,6 +65,7 @@ export interface Props<S> {
searchPlaceholder: string;
showLessAriaLabel?: string;
showMoreAriaLabel?: string;
+ showStatBar?: boolean;
stats: Dict<number> | undefined;
values: string[];
}
@@ -155,7 +158,7 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
};
handleHeaderClick = () => {
- this.props.onToggle(this.props.property);
+ this.props.onToggle?.(this.props.property);
};
handleClear = () => {
@@ -241,6 +244,24 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
return stats?.[item];
}
+ getStatBarPercent = (item: string) => {
+ const { showStatBar, stats } = this.props;
+ const { searchResultsCounts } = this.state;
+
+ if (!showStatBar) {
+ return undefined;
+ }
+
+ const combined = { ...stats, ...searchResultsCounts };
+
+ const maxFacetValue = max(values(combined));
+ const facetValue = combined?.[item];
+
+ return isDefined(facetValue) && isDefined(maxFacetValue) && maxFacetValue > 0
+ ? facetValue / maxFacetValue
+ : undefined;
+ };
+
getFacetHeaderId = (property: string) => {
return `facet_${property}`;
};
@@ -255,6 +276,7 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
renderList() {
const {
+ disableZero,
maxInitialItems,
maxItems,
property,
@@ -293,11 +315,13 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
{limitedList.map((item) => (
<FacetItem
active={this.props.values.includes(item)}
+ disableZero={disableZero}
className="it__search-navigator-facet"
key={item}
name={this.props.renderFacetItem(item)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(item)) ?? 0}
+ statBarPercent={this.getStatBarPercent(item)}
tooltip={this.props.getFacetItemText(item)}
value={item}
/>
@@ -313,10 +337,12 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
<FacetItem
active
className="it__search-navigator-facet"
+ disableZero={disableZero}
key={item}
name={this.props.renderFacetItem(item)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(item)) ?? 0}
+ statBarPercent={this.getStatBarPercent(item)}
tooltip={this.props.getFacetItemText(item)}
value={item}
/>
@@ -344,6 +370,7 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
}
renderSearch() {
+ const { minSearchLength } = this.props;
return (
<InputSearch
className="it__search-box-input sw-mb-4 sw-w-full"
@@ -354,6 +381,8 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
value={this.state.query}
searchInputAriaLabel={translate('search_verb')}
clearIconAriaLabel={translate('clear')}
+ minLength={minSearchLength}
+ tooShortText={translateWithParameters('select2.tooShort', minSearchLength)}
/>
);
}
@@ -399,6 +428,7 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
}
renderSearchResult(result: S) {
+ const { disableZero } = this.props;
const key = this.props.getSearchResultKey(result);
const active = this.props.values.includes(key);
const stat = formatFacetStat(this.getStat(key) ?? this.state.searchResultsCounts[key]) ?? 0;
@@ -407,10 +437,12 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
<FacetItem
active={active}
className="it__search-navigator-facet"
+ disableZero={disableZero}
key={key}
name={this.props.renderSearchResult(result, this.state.query)}
onClick={this.handleItemClick}
stat={stat}
+ statBarPercent={this.getStatBarPercent(key)}
tooltip={this.props.getSearchResultText(result)}
value={key}
/>
@@ -423,6 +455,7 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
facetHeader,
fetching,
inner,
+ minSearchLength,
open,
property,
stats = {},
@@ -436,7 +469,7 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
const loadingResults =
query !== '' && searching && (searchResults === undefined || searchResults.length === 0);
- const showList = !query || loadingResults;
+ const showList = query.length < minSearchLength || loadingResults;
const nbSelectableItems = Object.keys(stats).length;
const nbSelectedItems = values.length;
@@ -447,13 +480,14 @@ export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
clearIconLabel={translate('clear')}
count={nbSelectedItems}
countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={property}
disabled={disabled}
id={this.getFacetHeaderId(property)}
inner={inner}
loading={fetching}
name={facetHeader}
onClear={this.handleClear}
- onClick={disabled ? undefined : this.handleHeaderClick}
+ onClick={disabled || !this.props.onToggle ? undefined : this.handleHeaderClick}
open={open && !disabled}
>
{!disabled && (
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap
index 1e996ba29e7..f3f1bf306ff 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap
@@ -6,6 +6,7 @@ exports[`should be disabled 1`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
disabled={true}
id="facet_foo"
loading={false}
@@ -21,6 +22,7 @@ exports[`should display all selected items 1`] = `
clearIconLabel="clear"
count={3}
countLabel="x_selected.3"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -35,10 +37,12 @@ exports[`should display all selected items 1`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value=""
/>
<FacetItemsList
@@ -103,6 +107,7 @@ exports[`should render 1`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -117,10 +122,12 @@ exports[`should render 1`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value=""
/>
<FacetItemsList
@@ -176,6 +183,7 @@ exports[`should search 1`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -190,10 +198,12 @@ exports[`should search 1`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value="query"
/>
<FacetItemsList
@@ -242,6 +252,7 @@ exports[`should search 2`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -256,10 +267,12 @@ exports[`should search 2`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value="query"
/>
<FacetItemsList
@@ -318,6 +331,7 @@ exports[`should search 3`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -332,10 +346,12 @@ exports[`should search 3`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value=""
/>
<FacetItemsList
@@ -391,6 +407,7 @@ exports[`should search 4`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -405,10 +422,12 @@ exports[`should search 4`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value="blabla"
/>
<div
@@ -430,6 +449,7 @@ exports[`should search 5`] = `
clearIconLabel="clear"
count={0}
countLabel="x_selected.0"
+ data-property="foo"
id="facet_foo"
loading={false}
name="facet header"
@@ -444,10 +464,12 @@ exports[`should search 5`] = `
autoFocus={false}
className="it__search-box-input sw-mb-4 sw-w-full"
clearIconAriaLabel="clear"
+ minLength={2}
onChange={[Function]}
placeholder="search for foo..."
searchInputAriaLabel="search_verb"
size="auto"
+ tooShortText="select2.tooShort.2"
value="blabla"
/>
<div
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
index aed7d00f060..3d8f61057b3 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
@@ -17,10 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { omitBy } from 'lodash';
+import { keyBy, mapValues, omitBy } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { useSearchParams } from 'react-router-dom';
+import { searchProjects } from '../../../api/components';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
@@ -41,7 +42,7 @@ import { CurrentUser, isLoggedIn } from '../../../types/users';
import { Query, hasFilterParams, parseUrlQuery } from '../query';
import '../styles.css';
import { Facets, Project } from '../types';
-import { SORTING_SWITCH, fetchProjects, parseSorting } from '../utils';
+import { SORTING_SWITCH, convertToQueryData, fetchProjects, parseSorting } from '../utils';
import PageHeader from './PageHeader';
import PageSidebar from './PageSidebar';
import ProjectsList from './ProjectsList';
@@ -192,6 +193,21 @@ export class AllProjects extends React.PureComponent<Props, State> {
save(LS_PROJECTS_SORT, asString);
};
+ loadSearchResultCount = (property: string, values: string[]) => {
+ const { isFavorite } = this.props;
+ const { query = {} } = this.state;
+
+ const data = convertToQueryData({ ...query, [property]: values }, isFavorite, {
+ ps: 1,
+ facets: property,
+ });
+
+ return searchProjects(data).then(({ facets }) => {
+ const values = facets.find((facet) => facet.property === property)?.values ?? [];
+ return mapValues(keyBy(values, 'val'), 'count');
+ });
+ };
+
stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
@@ -224,6 +240,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
ComponentQualifier.Application
)}
facets={this.state.facets}
+ loadSearchResultCount={this.loadSearchResultCount}
onClearAll={this.handleClearAll}
onQueryChange={this.updateLocationQuery}
query={this.state.query}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ClearAll.tsx b/server/sonar-web/src/main/js/apps/projects/components/ClearAll.tsx
deleted file mode 100644
index d2eb0ad571f..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/components/ClearAll.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { Button } from '../../../components/controls/buttons';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
- onClearAll: () => void;
-}
-
-export default function ClearAll({ onClearAll }: Props) {
- return (
- <div className="projects-facets-reset">
- <Button className="button-red" onClick={onClearAll}>
- {translate('clear_all_filters')}
- </Button>
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
index f94818b304b..9d390002bfa 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ToggleButton } from 'design-system';
import * as React from 'react';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import ButtonToggle from '../../../components/controls/ButtonToggle';
import { withRouter, WithRouterProps } from '../../../components/hoc/withRouter';
import { translate } from '../../../helpers/l10n';
import { save } from '../../../helpers/storage';
@@ -62,13 +62,13 @@ export class FavoriteFilter extends React.PureComponent<Props> {
}
return (
- <div className="page-header text-center display-flex-justify-center">
- <ButtonToggle
+ <div className="sw-mb-8">
+ <ToggleButton
options={[
{ value: true, label: translate('my_favorites') },
{ value: false, label: translate('all') },
]}
- onCheck={this.onFavoriteChange}
+ onChange={this.onFavoriteChange}
value={pathname === FAVORITE_PATHNAME}
/>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
index c3c26c46620..01b7dfee04c 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
@@ -17,10 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { BasicSeparator, DangerButtonSecondary, StyledPageTitle } from 'design-system';
import { flatMap } from 'lodash';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import { RawQuery } from '../../../types/types';
+import { Dict, RawQuery } from '../../../types/types';
import CoverageFilter from '../filters/CoverageFilter';
import DuplicationsFilter from '../filters/DuplicationsFilter';
import LanguagesFilter from '../filters/LanguagesFilter';
@@ -31,21 +32,21 @@ import NewLinesFilter from '../filters/NewLinesFilter';
import NewMaintainabilityFilter from '../filters/NewMaintainabilityFilter';
import NewReliabilityFilter from '../filters/NewReliabilityFilter';
import NewSecurityFilter from '../filters/NewSecurityFilter';
-import QualifierFilter from '../filters/QualifierFilter';
-import QualityGateFilter from '../filters/QualityGateFilter';
+import QualifierFacet from '../filters/QualifierFilter';
+import QualityGateFacet from '../filters/QualityGateFilter';
import ReliabilityFilter from '../filters/ReliabilityFilter';
import SecurityFilter from '../filters/SecurityFilter';
import SecurityReviewFilter from '../filters/SecurityReviewFilter';
import SizeFilter from '../filters/SizeFilter';
-import TagsFilter from '../filters/TagsFilter';
+import TagsFacet from '../filters/TagsFilter';
import { hasFilterParams } from '../query';
import { Facets } from '../types';
-import ClearAll from './ClearAll';
import FavoriteFilter from './FavoriteFilter';
export interface PageSidebarProps {
applicationsEnabled: boolean;
facets?: Facets;
+ loadSearchResultCount: (property: string, values: string[]) => Promise<Dict<number>>;
onClearAll: () => void;
onQueryChange: (change: RawQuery) => void;
query: RawQuery;
@@ -53,7 +54,15 @@ export interface PageSidebarProps {
}
export default function PageSidebar(props: PageSidebarProps) {
- const { applicationsEnabled, facets, onClearAll, onQueryChange, query, view } = props;
+ const {
+ applicationsEnabled,
+ facets,
+ loadSearchResultCount,
+ onClearAll,
+ onQueryChange,
+ query,
+ view,
+ } = props;
const isFiltered = hasFilterParams(query);
const isLeakView = view === 'leak';
const maxFacetValue = getMaxFacetValue(facets);
@@ -69,17 +78,31 @@ export default function PageSidebar(props: PageSidebarProps) {
}, [onClearAll, heading]);
return (
- <div className="huge-spacer-bottom huge-padded-bottom">
+ <div className="sw-body-sm sw-pb-24">
<FavoriteFilter />
- <div className="projects-facets-header clearfix">
- {isFiltered && <ClearAll onClearAll={clearAll} />}
-
- <h2 className="h3" tabIndex={-1} ref={heading}>
+ <div className="sw-flex sw-items-center sw-justify-between">
+ <StyledPageTitle className="sw-body-md-highlight" as="h2" tabIndex={-1} ref={heading}>
{translate('filters')}
- </h2>
+ </StyledPageTitle>
+
+ {isFiltered && (
+ <DangerButtonSecondary onClick={clearAll}>
+ {translate('clear_all_filters')}
+ </DangerButtonSecondary>
+ )}
</div>
- <QualityGateFilter {...facetProps} facet={getFacet(facets, 'gate')} value={query.gate} />
+
+ <BasicSeparator className="sw-my-4" />
+
+ <QualityGateFacet
+ {...facetProps}
+ facet={getFacet(facets, 'gate')}
+ value={query.gate?.split(',')}
+ />
+
+ <BasicSeparator className="sw-my-4" />
+
{!isLeakView && (
<>
<ReliabilityFilter
@@ -87,33 +110,49 @@ export default function PageSidebar(props: PageSidebarProps) {
facet={getFacet(facets, 'reliability')}
value={query.reliability}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<SecurityFilter
{...facetProps}
facet={getFacet(facets, 'security')}
value={query.security}
/>
+ <BasicSeparator className="sw-my-4" />
+
<SecurityReviewFilter
{...facetProps}
facet={getFacet(facets, 'security_review')}
value={query.security_review_rating}
/>
+ <BasicSeparator className="sw-my-4" />
+
<MaintainabilityFilter
{...facetProps}
facet={getFacet(facets, 'maintainability')}
value={query.maintainability}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<CoverageFilter
{...facetProps}
facet={getFacet(facets, 'coverage')}
value={query.coverage}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<DuplicationsFilter
{...facetProps}
facet={getFacet(facets, 'duplications')}
value={query.duplications}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<SizeFilter {...facetProps} facet={getFacet(facets, 'size')} value={query.size} />
</>
)}
@@ -124,33 +163,50 @@ export default function PageSidebar(props: PageSidebarProps) {
facet={getFacet(facets, 'new_reliability')}
value={query.new_reliability}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<NewSecurityFilter
{...facetProps}
facet={getFacet(facets, 'new_security')}
value={query.new_security}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<SecurityReviewFilter
{...facetProps}
- className="leak-facet-box"
facet={getFacet(facets, 'new_security_review')}
property="new_security_review"
value={query.new_security_review_rating}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<NewMaintainabilityFilter
{...facetProps}
facet={getFacet(facets, 'new_maintainability')}
value={query.new_maintainability}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<NewCoverageFilter
{...facetProps}
facet={getFacet(facets, 'new_coverage')}
value={query.new_coverage}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<NewDuplicationsFilter
{...facetProps}
facet={getFacet(facets, 'new_duplications')}
value={query.new_duplications}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<NewLinesFilter
{...facetProps}
facet={getFacet(facets, 'new_lines')}
@@ -158,22 +214,34 @@ export default function PageSidebar(props: PageSidebarProps) {
/>
</>
)}
+
+ <BasicSeparator className="sw-my-4" />
+
<LanguagesFilter
{...facetProps}
facet={getFacet(facets, 'languages')}
+ loadSearchResultCount={loadSearchResultCount}
query={query}
value={query.languages}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
{applicationsEnabled && (
- <QualifierFilter
- {...facetProps}
- facet={getFacet(facets, 'qualifier')}
- value={query.qualifier}
- />
+ <>
+ <QualifierFacet
+ {...facetProps}
+ facet={getFacet(facets, 'qualifier')}
+ value={query.qualifier}
+ />
+
+ <BasicSeparator className="sw-my-4" />
+ </>
)}
- <TagsFilter
+ <TagsFacet
{...facetProps}
facet={getFacet(facets, 'tags')}
+ loadSearchResultCount={loadSearchResultCount}
query={query}
value={query.tags}
/>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
index 88c55dc28d0..04f2d3b99db 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
@@ -160,7 +160,7 @@ it('handles updating the favorite status of a project', () => {
it('handles showing favorite projects on load', () => {
const wrapper = shallowRender({
- props: { currentUser: { dismissedNotices: {}, isLoggedIn: false }, isFavorite: true },
+ props: { isFavorite: true },
});
expect(wrapper.state('projects')).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ClearAll-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ClearAll-test.tsx
deleted file mode 100644
index aacd61332c9..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ClearAll-test.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { click } from '../../../../helpers/testUtils';
-import ClearAll from '../ClearAll';
-
-it('renders', () => {
- expect(shallow(<ClearAll onClearAll={jest.fn()} />)).toMatchSnapshot();
-});
-
-it('clears all', () => {
- const onClearAll = jest.fn();
- const wrapper = shallow(<ClearAll onClearAll={onClearAll} />);
- click(wrapper.find('Button'));
- expect(onClearAll).toHaveBeenCalled();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx
index 892fb1c1eda..9b7f857ddb2 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx
@@ -31,15 +31,11 @@ it('should render the right facets for overview', () => {
query: { size: '3' },
});
- expect(screen.getByRole('heading', { level: 3, name: 'metric_domain.Size' })).toBeInTheDocument();
+ expect(screen.getByText('metric_domain.Size')).toBeInTheDocument();
- expect(
- screen.getByRole('heading', { level: 3, name: 'projects.facets.qualifier' })
- ).toBeInTheDocument();
+ expect(screen.getByText('projects.facets.qualifier')).toBeInTheDocument();
- expect(
- screen.queryByRole('heading', { level: 3, name: 'projects.facets.new_lines' })
- ).not.toBeInTheDocument();
+ expect(screen.queryByText('projects.facets.new_lines')).not.toBeInTheDocument();
});
it('should not show the qualifier facet with no applications', () => {
@@ -48,9 +44,7 @@ it('should not show the qualifier facet with no applications', () => {
query: { size: '3' },
});
- expect(
- screen.queryByRole('heading', { level: 3, name: 'projects.facets.qualifier' })
- ).not.toBeInTheDocument();
+ expect(screen.queryByText('projects.facets.qualifier')).not.toBeInTheDocument();
});
it('should show "new lines" instead of "size" when in `leak` view', () => {
@@ -59,13 +53,8 @@ it('should show "new lines" instead of "size" when in `leak` view', () => {
view: 'leak',
});
- expect(
- screen.queryByRole('heading', { level: 3, name: 'metric_domain.Size' })
- ).not.toBeInTheDocument();
-
- expect(
- screen.getByRole('heading', { level: 3, name: 'projects.facets.new_lines' })
- ).toBeInTheDocument();
+ expect(screen.queryByText('metric_domain.Size')).not.toBeInTheDocument();
+ expect(screen.getByText('projects.facets.new_lines')).toBeInTheDocument();
});
it('should allow to clear all filters', async () => {
@@ -98,6 +87,7 @@ function renderPageSidebar(overrides: Partial<PageSidebarProps> = {}, currentUse
>
<PageSidebar
applicationsEnabled
+ loadSearchResultCount={jest.fn().mockResolvedValue({})}
onClearAll={jest.fn()}
onQueryChange={jest.fn()}
query={{ view: 'overall' }}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ClearAll-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ClearAll-test.tsx.snap
deleted file mode 100644
index 2340e0bbf03..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ClearAll-test.tsx.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
- className="projects-facets-reset"
->
- <Button
- className="button-red"
- onClick={[MockFunction]}
- >
- clear_all_filters
- </Button>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx
index 057010671c7..9c2e07103ff 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx
@@ -17,17 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { CoverageIndicator } from 'design-system';
import * as React from 'react';
-import CoverageRating from '../../../components/ui/CoverageRating';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getCoverageRatingAverageValue, getCoverageRatingLabel } from '../../../helpers/ratings';
+import { MetricKey } from '../../../types/metrics';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
+import RangeFacetBase from './RangeFacetBase';
export interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -38,23 +37,22 @@ export interface Props {
const NO_DATA_OPTION = 6;
export default function CoverageFilter(props: Props) {
- const { property = 'coverage' } = props;
+ const { facet, maxFacetValue, property = MetricKey.coverage, value } = props;
return (
- <Filter
- className={props.className}
- facet={props.facet}
+ <RangeFacetBase
+ facet={facet}
getFacetValueForOption={getFacetValueForOption}
- header={<FilterHeader name={translate('metric_domain.Coverage')} />}
+ header={translate('metric_domain.Coverage')}
highlightUnder={1}
highlightUnderMax={5}
- maxFacetValue={props.maxFacetValue}
+ maxFacetValue={maxFacetValue}
onQueryChange={props.onQueryChange}
options={[1, 2, 3, 4, 5, 6]}
property={property}
renderAccessibleLabel={renderAccessibleLabel}
renderOption={renderOption}
- value={props.value}
+ value={value}
/>
);
}
@@ -73,17 +71,17 @@ function renderAccessibleLabel(option: number) {
);
}
-function renderOption(option: number, selected: boolean) {
+function renderOption(option: number) {
return (
- <div className="display-flex-center">
+ <div className="sw-flex sw-items-center">
{option < NO_DATA_OPTION && (
- <CoverageRating muted={!selected} value={getCoverageRatingAverageValue(option)} />
+ <CoverageIndicator value={getCoverageRatingAverageValue(option)} size="xs" />
)}
- <span className="spacer-left">
+ <span className="sw-ml-2">
{option < NO_DATA_OPTION ? (
getCoverageRatingLabel(option)
) : (
- <span className="big-spacer-left">{translate('no_data')}</span>
+ <span className="sw-ml-4">{translate('no_data')}</span>
)}
</span>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.tsx
index fc4582488a3..d8b6032a639 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.tsx
@@ -17,20 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DuplicationsIndicator } from 'design-system';
import * as React from 'react';
-import DuplicationsRating from '../../../components/ui/DuplicationsRating';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import {
- getDuplicationsRatingAverageValue,
- getDuplicationsRatingLabel,
-} from '../../../helpers/ratings';
+import { duplicationValueToRating, getDuplicationsRatingLabel } from '../../../helpers/ratings';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
+import RangeFacetBase from './RangeFacetBase';
export interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -41,22 +36,21 @@ export interface Props {
const NO_DATA_OPTION = 6;
export default function DuplicationsFilter(props: Props) {
- const { property = 'duplications' } = props;
+ const { facet, maxFacetValue, property = 'duplications', value } = props;
return (
- <Filter
- className={props.className}
- facet={props.facet}
+ <RangeFacetBase
+ facet={facet}
getFacetValueForOption={getFacetValueForOption}
- header={<FilterHeader name={translate('metric_domain.Duplications')} />}
+ header={translate('metric_domain.Duplications')}
highlightUnder={1}
highlightUnderMax={5}
- maxFacetValue={props.maxFacetValue}
+ maxFacetValue={maxFacetValue}
onQueryChange={props.onQueryChange}
options={[1, 2, 3, 4, 5, 6]}
property={property}
renderAccessibleLabel={renderAccessibleLabel}
renderOption={renderOption}
- value={props.value}
+ value={value}
/>
);
}
@@ -75,21 +69,18 @@ function renderAccessibleLabel(option: number) {
);
}
-function renderOption(option: number, selected: boolean) {
+function renderOption(option: number) {
return (
- <div className="display-flex-center">
+ <div className="sw-flex sw-items-center">
{option < NO_DATA_OPTION && (
- <DuplicationsRating
- muted={!selected}
- size="small"
- value={getDuplicationsRatingAverageValue(option)}
- />
+ /* Adjust option to skip the 0 */
+ <DuplicationsIndicator size="xs" rating={duplicationValueToRating(option + 1)} />
)}
- <span className="spacer-left">
+ <span className="sw-ml-2">
{option < NO_DATA_OPTION ? (
getDuplicationsRatingLabel(option)
) : (
- <span className="big-spacer-left">{translate('no_data')}</span>
+ <span className="sw-ml-4">{translate('no_data')}</span>
)}
</span>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx
deleted file mode 100644
index a54958d3056..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-import { MetricType } from '../../../types/metrics';
-import { RawQuery } from '../../../types/types';
-import { Facet } from '../types';
-
-export type Option = string | number;
-
-interface Props {
- property: string;
- className?: string;
- onQueryChange: (change: RawQuery) => void;
- options: Option[];
- renderAccessibleLabel: (option: Option) => string;
- renderOption: (option: Option, isSelected: boolean) => React.ReactNode;
-
- value?: Option | Option[];
- facet?: Facet;
- maxFacetValue?: number;
- optionClassName?: string;
-
- getFacetValueForOption?: (facet: Facet, option: Option) => number;
-
- halfWidth?: boolean;
- highlightUnder?: number;
- highlightUnderMax?: number;
-
- header?: React.ReactNode;
- footer?: React.ReactNode;
-}
-
-const defaultGetFacetValueForOption = (facet: Facet, option: string | number) => facet[option];
-
-export default class Filter extends React.PureComponent<Props> {
- isSelected(option: Option): boolean {
- const { value } = this.props;
-
- return Array.isArray(value) ? value.includes(option) : String(option) === String(value);
- }
-
- highlightUnder(option?: number): boolean {
- return (
- this.props.highlightUnder !== undefined &&
- option !== undefined &&
- option > this.props.highlightUnder &&
- (this.props.highlightUnderMax == null || option < this.props.highlightUnderMax)
- );
- }
-
- getUrlOptionForSingleValue = (option: string) => (this.isSelected(option) ? null : option);
-
- getUrlOptionForMultiValue = (
- event: React.MouseEvent<HTMLButtonElement>,
- option: string,
- value: Option[]
- ) => {
- if (event.ctrlKey || event.metaKey) {
- if (this.isSelected(option)) {
- return value.length > 1 ? value.filter((val) => val !== option).join(',') : null;
- }
-
- return value.concat(option).join(',');
- }
-
- return this.isSelected(option) && value.length < 2 ? null : option;
- };
-
- handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
- event.preventDefault();
-
- const { property, value } = this.props;
- const { key: option } = event.currentTarget.dataset;
-
- if (option) {
- const urlOption = Array.isArray(value)
- ? this.getUrlOptionForMultiValue(event, option, value)
- : this.getUrlOptionForSingleValue(option);
-
- this.props.onQueryChange({ [property]: urlOption });
- }
- };
-
- renderOptionBar(facetValue: number | undefined) {
- if (facetValue === undefined || !this.props.maxFacetValue) {
- return null;
- }
-
- return (
- <div className="projects-facet-bar">
- <div
- className="projects-facet-bar-inner"
- style={{ width: (facetValue / this.props.maxFacetValue) * 60 }}
- />
- </div>
- );
- }
-
- renderOption(option: Option, highlightable = false, lastHighlightable = false) {
- const { facet, getFacetValueForOption = defaultGetFacetValueForOption, value } = this.props;
- const active = this.isSelected(option);
-
- const className = classNames(
- 'facet',
- 'search-navigator-facet',
- 'projects-facet',
- 'button-link',
- {
- active,
- 'search-navigator-facet-half': this.props.halfWidth,
- },
- this.props.optionClassName
- );
-
- const facetValue =
- facet && getFacetValueForOption ? getFacetValueForOption(facet, option) : undefined;
-
- const isUnderSelectedOption =
- typeof value === 'number' &&
- typeof option === 'number' &&
- this.highlightUnder(value) &&
- option > value;
-
- return (
- <li
- key={option}
- className={classNames({
- 'search-navigator-facet-worse-than-highlight': highlightable,
- last: lastHighlightable,
- active,
- })}
- >
- <button
- aria-label={this.props.renderAccessibleLabel(option)}
- className={className}
- data-key={option}
- type="button"
- tabIndex={0}
- onClick={this.handleClick}
- role="checkbox"
- aria-checked={this.isSelected(option) || isUnderSelectedOption}
- >
- <span className="facet-name">
- {this.props.renderOption(option, this.isSelected(option) || isUnderSelectedOption)}
- </span>
- {facetValue != null && (
- <span className="facet-stat">
- {formatMeasure(facetValue, MetricType.ShortInteger)}
- {this.renderOptionBar(facetValue)}
- </span>
- )}
- </button>
- </li>
- );
- }
-
- renderOptions = () => {
- const { options, highlightUnder } = this.props;
-
- if (options && options.length > 0) {
- if (highlightUnder != null) {
- const max = this.props.highlightUnderMax || options.length;
- const beforeHighlight = options.slice(0, highlightUnder);
- const insideHighlight = options.slice(highlightUnder, max);
- const afterHighlight = options.slice(max);
-
- return (
- <ul className="search-navigator-facet-list projects-facet-list">
- {beforeHighlight.map((option) => this.renderOption(option))}
- {insideHighlight.map((option, i) =>
- this.renderOption(option, true, i === insideHighlight.length - 1)
- )}
- {afterHighlight.map((option) => this.renderOption(option))}
- </ul>
- );
- }
-
- return (
- <ul className="search-navigator-facet-list projects-facet-list">
- {options.map((option) => this.renderOption(option))}
- </ul>
- );
- }
-
- return (
- <div className="search-navigator-facet-empty">
- <em>{translate('projects.facets.no_available_filters_clear_others')}</em>
- </div>
- );
- };
-
- render() {
- return (
- <div
- className={classNames('search-navigator-facet-box', this.props.className)}
- data-key={this.props.property}
- >
- {this.props.header}
- {this.renderOptions()}
- {this.props.footer}
- </div>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.tsx b/server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.tsx
deleted file mode 100644
index c9a4999f0ea..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 * as React from 'react';
-
-interface Props {
- children?: React.ReactNode;
- name: string;
-}
-
-export default function FilterHeader(props: Props) {
- return (
- <div className="search-navigator-facet-header projects-facet-header">
- <h3 className="h4">{props.name}</h3>
- {props.children}
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx
index db37e1dbe78..b2a5f56c2ab 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx
@@ -17,79 +17,110 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { difference, sortBy } from 'lodash';
+import { uniqBy } from 'lodash';
import * as React from 'react';
import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Languages } from '../../../types/languages';
+import { translate } from '../../../helpers/l10n';
+import { highlightTerm } from '../../../helpers/search';
+import { Language, Languages } from '../../../types/languages';
import { Dict, RawQuery } from '../../../types/types';
+import { ListStyleFacet } from '../../issues/sidebar/ListStyleFacet';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
-import SearchableFilterFooter from './SearchableFilterFooter';
-import SearchableFilterOption from './SearchableFilterOption';
interface Props {
facet?: Facet;
languages: Languages;
- maxFacetValue?: number;
+ loadSearchResultCount: (property: string, values: string[]) => Promise<Dict<number>>;
onQueryChange: (change: RawQuery) => void;
- property?: string;
query: Dict<any>;
value?: string[];
}
-export class LanguagesFilter extends React.Component<Props> {
- getSearchOptions = () => {
- const { facet, languages } = this.props;
- let languageKeys = Object.keys(languages);
- if (facet) {
- languageKeys = difference(languageKeys, Object.keys(facet));
- }
- return languageKeys.map((key) => ({ label: languages[key].name, value: key }));
- };
+export function LanguagesFilter(props: Props) {
+ const { facet, languages, loadSearchResultCount, query, onQueryChange, value = [] } = props;
- getSortedOptions = (facet: Facet = {}) =>
- sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]);
-
- renderAccessibleLabel = (option: string) => {
- const { languages } = this.props;
- return translateWithParameters(
- 'projects.facets.label_text_x',
- translate('projects.facets.languages'),
- languages[option]?.name || option
+ const searchOptions = React.useMemo(() => {
+ // add any language that presents in the facet, but might not be installed
+ // for such language we don't know their display name, so let's just use their key
+ // and make sure we reference each language only once
+ return uniqBy(
+ [...Object.values(languages), ...Object.keys(facet ?? {}).map((key) => ({ key, name: key }))],
+ (language) => language.key
);
- };
+ }, [languages, facet]);
- renderOption = (option: string) => (
- <SearchableFilterOption option={this.props.languages[option]} optionKey={option} />
+ const handleChange = React.useCallback(
+ (newValue: Dict<string[]>) => {
+ const { languages } = newValue;
+ onQueryChange({ languages: languages.join(',') });
+ },
+ [onQueryChange]
);
- render() {
- const { property = 'languages' } = this.props;
+ const handleSearch = React.useCallback(
+ (query: string) => {
+ const results = searchOptions.filter((lang) =>
+ lang.name.toLowerCase().includes(query.toLowerCase())
+ );
- return (
- <Filter
- facet={this.props.facet}
- footer={
- <SearchableFilterFooter
- onQueryChange={this.props.onQueryChange}
- options={this.getSearchOptions()}
- property={property}
- query={this.props.query}
- />
- }
- header={<FilterHeader name={translate('projects.facets.languages')} />}
- maxFacetValue={this.props.maxFacetValue}
- onQueryChange={this.props.onQueryChange}
- options={this.getSortedOptions(this.props.facet)}
- property={property}
- renderAccessibleLabel={this.renderAccessibleLabel}
- renderOption={this.renderOption}
- value={this.props.value}
- />
- );
- }
+ const paging = { pageIndex: 1, pageSize: results.length, total: results.length };
+
+ return Promise.resolve({
+ paging,
+ results,
+ });
+ },
+ [searchOptions]
+ );
+
+ const handleSearchResultCount = React.useCallback(
+ (languages: Language[]) => {
+ return loadSearchResultCount(
+ 'languages',
+ languages.map((l) => l.key)
+ );
+ },
+ [loadSearchResultCount]
+ );
+
+ const renderSearchResults = React.useCallback(
+ (lang: Language, term: string) => highlightTerm(lang.name, term),
+ []
+ );
+
+ const renderLanguageName = React.useCallback(
+ (key: string) => {
+ if (key === '<null>') {
+ return translate('unknown');
+ }
+
+ return languages[key]?.name || key;
+ },
+ [languages]
+ );
+
+ return (
+ <ListStyleFacet<Language>
+ facetHeader={translate('projects.facets.languages')}
+ fetching={false}
+ getFacetItemText={renderLanguageName}
+ getSearchResultKey={(language) => language.key}
+ getSearchResultText={(language) => language.name}
+ loadSearchResultCount={handleSearchResultCount}
+ minSearchLength={1}
+ onChange={handleChange}
+ onSearch={handleSearch}
+ query={query}
+ open
+ property="languages"
+ renderFacetItem={renderLanguageName}
+ renderSearchResult={renderSearchResults}
+ searchPlaceholder={translate('search.search_for_languages')}
+ showStatBar
+ stats={facet}
+ values={value}
+ />
+ );
}
export default withLanguagesContext(LanguagesFilter);
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx
index 6608a4b6888..f917d19f579 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx
@@ -18,11 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import CodeSmellIcon from '../../../components/icons/CodeSmellIcon';
-import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import IssuesFilter from './IssuesFilter';
+import RatingFacet from './RatingFacet';
interface Props {
className?: string;
@@ -34,19 +32,5 @@ interface Props {
}
export default function MaintainabilityFilter(props: Props) {
- return (
- <IssuesFilter
- {...props}
- headerDetail={
- <span className="note little-spacer-left">
- {'( '}
- <CodeSmellIcon className="little-spacer-right" />
- {translate('metric.code_smells.name')}
- {' )'}
- </span>
- }
- name="Maintainability"
- property="maintainability"
- />
- );
+ return <RatingFacet {...props} name="Maintainability" property="maintainability" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewCoverageFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewCoverageFilter.tsx
index 5afadaa3c49..7bd8e439be2 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/NewCoverageFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/NewCoverageFilter.tsx
@@ -18,8 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { MetricKey } from '../../../types/metrics';
import CoverageFilter, { Props } from './CoverageFilter';
export default function NewCoverageFilter(props: Props) {
- return <CoverageFilter {...props} className="leak-facet-box" property="new_coverage" />;
+ return <CoverageFilter {...props} property={MetricKey.new_coverage} />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewDuplicationsFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewDuplicationsFilter.tsx
index 768b27279b6..ab5b76ecd0c 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/NewDuplicationsFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/NewDuplicationsFilter.tsx
@@ -21,5 +21,5 @@ import * as React from 'react';
import DuplicationsFilter, { Props } from './DuplicationsFilter';
export default function NewDuplicationsFilter(props: Props) {
- return <DuplicationsFilter {...props} className="leak-facet-box" property="new_duplications" />;
+ return <DuplicationsFilter {...props} property="new_duplications" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx
index 3a8280ecdd5..d6e2d1e6ae1 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx
@@ -20,10 +20,10 @@
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { getSizeRatingLabel } from '../../../helpers/ratings';
+import { MetricKey } from '../../../types/metrics';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
+import RangeFacetBase from './RangeFacetBase';
export interface Props {
facet?: Facet;
@@ -34,22 +34,21 @@ export interface Props {
}
export default function NewLinesFilter(props: Props) {
- const { property = 'new_lines' } = props;
+ const { facet, maxFacetValue, property = MetricKey.new_lines, value } = props;
return (
- <Filter
- className="leak-facet-box"
- facet={props.facet}
+ <RangeFacetBase
+ facet={facet}
getFacetValueForOption={getFacetValueForOption}
- header={<FilterHeader name={translate('projects.facets.new_lines')} />}
+ header={translate('projects.facets.new_lines')}
highlightUnder={1}
- maxFacetValue={props.maxFacetValue}
+ maxFacetValue={maxFacetValue}
onQueryChange={props.onQueryChange}
options={[1, 2, 3, 4, 5]}
property={property}
renderAccessibleLabel={renderAccessibleLabel}
renderOption={renderOption}
- value={props.value}
+ value={value}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx
index e77b0e80c63..c2107c47736 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx
@@ -18,14 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import CodeSmellIcon from '../../../components/icons/CodeSmellIcon';
-import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import IssuesFilter from './IssuesFilter';
+import RatingFacet from './RatingFacet';
interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -33,20 +30,5 @@ interface Props {
}
export default function NewMaintainabilityFilter(props: Props) {
- return (
- <IssuesFilter
- {...props}
- className="leak-facet-box"
- headerDetail={
- <span className="note little-spacer-left">
- {'( '}
- <CodeSmellIcon className="little-spacer-right" />
- {translate('metric.code_smells.name')}
- {' )'}
- </span>
- }
- name="Maintainability"
- property="new_maintainability"
- />
- );
+ return <RatingFacet {...props} name="Maintainability" property="new_maintainability" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx
index 4af0e6b8b4a..8ae479e2771 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx
@@ -18,14 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import BugIcon from '../../../components/icons/BugIcon';
-import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import IssuesFilter from './IssuesFilter';
+import RatingFacet from './RatingFacet';
interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -33,20 +30,5 @@ interface Props {
}
export default function NewReliabilityFilter(props: Props) {
- return (
- <IssuesFilter
- {...props}
- className="leak-facet-box"
- headerDetail={
- <span className="note little-spacer-left">
- {'( '}
- <BugIcon className="little-spacer-right" />
- {translate('metric.bugs.name')}
- {' )'}
- </span>
- }
- name="Reliability"
- property="new_reliability"
- />
- );
+ return <RatingFacet {...props} name="Reliability" property="new_reliability" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx
index 653a379f98a..d7732c9de6b 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx
@@ -18,14 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import VulnerabilityIcon from '../../../components/icons/VulnerabilityIcon';
-import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import IssuesFilter from './IssuesFilter';
+import RatingFacet from './RatingFacet';
interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -33,20 +30,5 @@ interface Props {
}
export default function NewSecurityFilter(props: Props) {
- return (
- <IssuesFilter
- {...props}
- className="leak-facet-box"
- headerDetail={
- <span className="note little-spacer-left">
- {'( '}
- <VulnerabilityIcon className="little-spacer-right" />
- {translate('metric.vulnerabilities.name')}
- {' )'}
- </span>
- }
- name="Security"
- property="new_security"
- />
- );
+ return <RatingFacet {...props} name="Security" property="new_security" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualifierFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/QualifierFilter.tsx
index b8488bb5eff..61e1ad5d685 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/QualifierFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/QualifierFilter.tsx
@@ -17,59 +17,70 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FacetBox, FacetItem } from 'design-system';
import * as React from 'react';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
import { ComponentQualifier } from '../../../types/component';
import { RawQuery } from '../../../types/types';
+import { FacetItemsList } from '../../issues/sidebar/FacetItemsList';
+import { formatFacetStat } from '../../issues/utils';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
-export interface QualifierFilterProps {
+export interface QualifierFacetProps {
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
value: ComponentQualifier | undefined;
}
+const HEADER_ID = `facet_qualifier`;
+
const options = [ComponentQualifier.Project, ComponentQualifier.Application];
-export default function QualifierFilter(props: QualifierFilterProps) {
- const { facet, maxFacetValue, value } = props;
+export default function QualifierFacet(props: QualifierFacetProps) {
+ const { facet, maxFacetValue, onQueryChange, value } = props;
- return (
- <Filter
- facet={facet}
- header={<FilterHeader name={translate('projects.facets.qualifier')} />}
- maxFacetValue={maxFacetValue}
- onQueryChange={props.onQueryChange}
- options={options}
- property="qualifier"
- renderAccessibleLabel={renderAccessibleLabel}
- renderOption={renderOption}
- value={value}
- />
- );
-}
+ const onItemClick = React.useCallback(
+ (itemValue: string) => {
+ const active = value === itemValue;
-function renderAccessibleLabel(option: string) {
- return translateWithParameters(
- 'projects.facets.label_text_x',
- translate('projects.facets.qualifier'),
- translate('qualifier', option)
+ onQueryChange({
+ qualifier: active ? '' : itemValue,
+ });
+ },
+ [onQueryChange, value]
);
-}
-function renderOption(option: string, selected: boolean) {
return (
- <span className="display-flex-center">
- <QualifierIcon
- className="spacer-right"
- fill={selected ? undefined : 'currentColor'}
- qualifier={option}
- />
- {translate('qualifier', option)}
- </span>
+ <FacetBox id={HEADER_ID} open name={translate('projects.facets.qualifier')}>
+ <FacetItemsList labelledby={HEADER_ID}>
+ {options.map((option) => {
+ const facetValue = facet?.[option];
+
+ const statBarPercent =
+ isDefined(facetValue) && isDefined(maxFacetValue) && maxFacetValue > 0
+ ? facetValue / maxFacetValue
+ : undefined;
+
+ return (
+ <FacetItem
+ disableZero={false}
+ key={option}
+ active={value === option}
+ name={renderOption(option)}
+ onClick={onItemClick}
+ value={option}
+ stat={formatFacetStat(facet?.[option]) ?? 0}
+ statBarPercent={statBarPercent}
+ />
+ );
+ })}
+ </FacetItemsList>
+ </FacetBox>
);
}
+
+function renderOption(option: string) {
+ return <div className="sw-flex sw-items-center">{translate('qualifier', option)}</div>;
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.tsx
index 047d95b42ab..b24b8b3469b 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.tsx
@@ -17,58 +17,87 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FacetBox, FacetItem, HelperHintIcon, QualityGateIndicator } from 'design-system';
+import { without } from 'lodash';
import * as React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
-import Level from '../../../components/ui/Level';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { RawQuery } from '../../../types/types';
+import { translate } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
+import { RawQuery, Status } from '../../../types/types';
+import { FacetItemsList } from '../../issues/sidebar/FacetItemsList';
+import { formatFacetStat } from '../../issues/utils';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
export interface Props {
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
- value?: any;
+ value?: Array<string>;
}
-function renderAccessibleLabel(option: string) {
- return translateWithParameters(
- 'projects.facets.qualitygate_label_x',
- translate('metric.level', option)
- );
-}
+const HEADER_ID = `facet_quality_gate`;
-export default function QualityGateFilter(props: Props) {
- const hasWarnStatus = props.facet && props.facet['WARN'] !== undefined;
+export default function QualityGateFacet(props: Props) {
+ const { facet, maxFacetValue, onQueryChange, value } = props;
+ const hasWarnStatus = facet?.['WARN'] !== undefined;
const options = hasWarnStatus ? ['OK', 'WARN', 'ERROR'] : ['OK', 'ERROR'];
+ const onItemClick = React.useCallback(
+ (itemValue: string, multiple: boolean) => {
+ const active = value?.includes(itemValue);
+
+ if (multiple) {
+ onQueryChange({
+ gate: (active ? without(value, itemValue) : [...(value ?? []), itemValue]).join(','),
+ });
+ } else {
+ onQueryChange({
+ gate: (active && value?.length === 1 ? [] : [itemValue]).join(','),
+ });
+ }
+ },
+ [onQueryChange, value]
+ );
+
return (
- <Filter
- facet={props.facet}
- header={<FilterHeader name={translate('projects.facets.quality_gate')} />}
- maxFacetValue={props.maxFacetValue}
- onQueryChange={props.onQueryChange}
- options={options}
- property="gate"
- renderOption={renderOption}
- renderAccessibleLabel={renderAccessibleLabel}
- value={props.value}
- />
+ <FacetBox id={HEADER_ID} open name={translate('projects.facets.quality_gate')}>
+ <FacetItemsList labelledby={HEADER_ID}>
+ {options.map((option) => {
+ const facetValue = facet?.[option];
+
+ const statBarPercent =
+ isDefined(facetValue) && isDefined(maxFacetValue) && maxFacetValue > 0
+ ? facetValue / maxFacetValue
+ : undefined;
+
+ return (
+ <FacetItem
+ disableZero={false}
+ key={option}
+ active={value?.includes(option)}
+ name={renderOption(option)}
+ onClick={onItemClick}
+ value={option}
+ stat={formatFacetStat(facet?.[option]) ?? 0}
+ statBarPercent={statBarPercent}
+ />
+ );
+ })}
+ </FacetItemsList>
+ </FacetBox>
);
}
-function renderOption(option: string, selected: boolean) {
+function renderOption(option: string) {
return (
- <>
- <Level level={option} muted={!selected} small />
+ <div className="sw-flex sw-items-center">
+ <QualityGateIndicator status={option as Status} size="sm" />
+ <span className="sw-ml-1">{translate('metric.level', option)}</span>
{option === 'WARN' && (
- <HelpTooltip
- className="little-spacer-left"
- overlay={translate('projects.facets.quality_gate.warning_help')}
- />
+ <HelpTooltip overlay={translate('projects.facets.quality_gate.warning_help')}>
+ <HelperHintIcon className="sw-ml-1" />
+ </HelpTooltip>
)}
- </>
+ </div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx b/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx
new file mode 100644
index 00000000000..c6eb173ccd5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx
@@ -0,0 +1,165 @@
+/*
+ * 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 { FacetBox, FacetItem, HighlightedFacetItems, LightLabel } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
+import { RawQuery } from '../../../types/types';
+import { FacetItemsList } from '../../issues/sidebar/FacetItemsList';
+import { formatFacetStat } from '../../issues/utils';
+import { Facet } from '../types';
+
+export type Option = string | number;
+
+interface Props {
+ property: string;
+ className?: string;
+ onQueryChange: (change: RawQuery) => void;
+ options: Option[];
+ renderAccessibleLabel: (option: Option) => string;
+ renderOption: (option: Option, isSelected: boolean) => React.ReactNode;
+
+ value?: Option;
+ facet?: Facet;
+ maxFacetValue?: number;
+ optionClassName?: string;
+
+ getFacetValueForOption?: (facet: Facet, option: Option) => number;
+
+ highlightUnder?: number;
+ highlightUnderMax?: number;
+
+ header: string;
+}
+
+const defaultGetFacetValueForOption = (facet: Facet, option: string | number) => facet[option];
+
+export default class RangeFacetBase extends React.PureComponent<Props> {
+ isSelected(option: Option): boolean {
+ const { value } = this.props;
+
+ return String(option) === String(value);
+ }
+
+ highlightUnder(option?: number): boolean {
+ return (
+ this.props.highlightUnder !== undefined &&
+ option !== undefined &&
+ option > this.props.highlightUnder &&
+ (this.props.highlightUnderMax == null || option < this.props.highlightUnderMax)
+ );
+ }
+
+ handleClick = (clicked: string) => {
+ const { property, onQueryChange, value } = this.props;
+
+ if (clicked === value?.toString()) {
+ onQueryChange({ [property]: undefined });
+ } else {
+ onQueryChange({
+ [property]: clicked,
+ });
+ }
+ };
+
+ renderOption(option: Option) {
+ const {
+ optionClassName,
+ facet,
+ getFacetValueForOption = defaultGetFacetValueForOption,
+ maxFacetValue,
+ value,
+ } = this.props;
+ const active = this.isSelected(option);
+
+ const facetValue =
+ facet && getFacetValueForOption ? getFacetValueForOption(facet, option) : undefined;
+
+ const isUnderSelectedOption =
+ typeof value === 'number' &&
+ typeof option === 'number' &&
+ this.highlightUnder(value) &&
+ option > value;
+
+ const statBarPercent =
+ isDefined(facetValue) && isDefined(maxFacetValue) && maxFacetValue > 0
+ ? facetValue / maxFacetValue
+ : undefined;
+
+ return (
+ <FacetItem
+ active={active}
+ disableZero={false}
+ aria-label={this.props.renderAccessibleLabel(option)}
+ key={option}
+ className={classNames({ active }, optionClassName)}
+ data-key={option}
+ onClick={this.handleClick}
+ name={this.props.renderOption(option, this.isSelected(option) || isUnderSelectedOption)}
+ stat={formatFacetStat(facetValue) ?? 0}
+ statBarPercent={statBarPercent}
+ value={option.toString()}
+ />
+ );
+ }
+
+ renderOptions = () => {
+ const { options, header, highlightUnder } = this.props;
+
+ if (options && options.length > 0) {
+ if (highlightUnder != null) {
+ const max = this.props.highlightUnderMax ?? options.length;
+ const beforeHighlight = options.slice(0, highlightUnder);
+ const insideHighlight = options.slice(highlightUnder, max);
+ const afterHighlight = options.slice(max);
+
+ return (
+ <FacetItemsList label={header}>
+ {beforeHighlight.map((option) => this.renderOption(option))}
+ <HighlightedFacetItems>
+ {insideHighlight.map((option) => this.renderOption(option))}
+ </HighlightedFacetItems>
+ {afterHighlight.map((option) => this.renderOption(option))}
+ </FacetItemsList>
+ );
+ }
+
+ return <ul>{options.map((option) => this.renderOption(option))}</ul>;
+ }
+
+ return (
+ <LightLabel>
+ <em>{translate('projects.facets.no_available_filters_clear_others')}</em>
+ </LightLabel>
+ );
+ };
+
+ render() {
+ const { className, header, property } = this.props;
+
+ return (
+ <FacetBox className={className} name={header} data-key={property} open>
+ {this.renderOptions()}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx
index cc2faf93300..99ce03912cb 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx
@@ -17,19 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { MetricsEnum, MetricsRatingBadge } from 'design-system';
import * as React from 'react';
-import Rating from '../../../components/ui/Rating';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
+import RangeFacetBase from './RangeFacetBase';
interface Props {
- className?: string;
facet?: Facet;
- headerDetail?: React.ReactNode;
maxFacetValue?: number;
name: string;
onQueryChange: (change: RawQuery) => void;
@@ -37,8 +35,8 @@ interface Props {
value?: any;
}
-export default function IssuesFilter(props: Props) {
- const { name } = props;
+export default function RatingFacet(props: Props) {
+ const { facet, maxFacetValue, name, property, value } = props;
const renderAccessibleLabel = React.useCallback(
(option: number) => {
@@ -46,47 +44,39 @@ export default function IssuesFilter(props: Props) {
return translateWithParameters(
'projects.facets.rating_label_single_x',
translate('metric_domain', name),
- formatMeasure(option, 'RATING')
+ formatMeasure(option, MetricType.Rating)
);
}
return translateWithParameters(
'projects.facets.rating_label_multi_x',
translate('metric_domain', name),
- formatMeasure(option, 'RATING')
+ formatMeasure(option, MetricType.Rating)
);
},
[name]
);
return (
- <Filter
- className={props.className}
- facet={props.facet}
- header={
- <FilterHeader name={translate('metric_domain', props.name)}>
- {props.headerDetail}
- </FilterHeader>
- }
+ <RangeFacetBase
+ facet={facet}
+ header={translate('metric_domain', name)}
highlightUnder={1}
- maxFacetValue={props.maxFacetValue}
+ maxFacetValue={maxFacetValue}
onQueryChange={props.onQueryChange}
options={[1, 2, 3, 4, 5]}
- property={props.property}
+ property={property}
renderAccessibleLabel={renderAccessibleLabel}
renderOption={renderOption}
- value={props.value}
+ value={value}
/>
);
}
-function renderOption(option: number, selected: boolean) {
+function renderOption(option: number) {
+ const ratingFormatted = formatMeasure(option, MetricType.Rating);
+
return (
- <span>
- <Rating muted={!selected} value={option} />
- <span className="spacer-left">
- {translateWithParameters('projects.facets.rating_x', formatMeasure(option, 'RATING'))}
- </span>
- </span>
+ <MetricsRatingBadge label={ratingFormatted} rating={ratingFormatted as MetricsEnum} size="xs" />
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx
index 23e2f15ce2d..6bcdb4d9871 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx
@@ -18,14 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import BugIcon from '../../../components/icons/BugIcon';
-import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import IssuesFilter from './IssuesFilter';
+import RatingFacet from './RatingFacet';
interface Props {
- className?: string;
facet?: Facet;
headerDetail?: React.ReactNode;
maxFacetValue?: number;
@@ -34,19 +31,5 @@ interface Props {
}
export default function ReliabilityFilter(props: Props) {
- return (
- <IssuesFilter
- {...props}
- headerDetail={
- <span className="note little-spacer-left">
- {'( '}
- <BugIcon className="little-spacer-right" />
- {translate('metric.bugs.name')}
- {' )'}
- </span>
- }
- name="Reliability"
- property="reliability"
- />
- );
+ return <RatingFacet {...props} name="Reliability" property="reliability" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.tsx
deleted file mode 100644
index 9adcdef1ce9..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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 * as React from 'react';
-import Select from '../../../components/controls/Select';
-import { translate } from '../../../helpers/l10n';
-import { Dict, RawQuery } from '../../../types/types';
-
-interface Props {
- isLoading?: boolean;
- onInputChange?: (query: string) => void;
- onOpen?: () => void;
- onQueryChange: (change: RawQuery) => void;
- options: Array<{ label: string; value: string }>;
- property: string;
- query: Dict<any>;
-}
-
-export default function SearchableFilterFooter(props: Props) {
- const { property, query } = props;
-
- const handleOptionChange = ({ value }: { value: string }) => {
- if (value) {
- const urlOptions = (query[property] || []).concat(value).join(',');
- props.onQueryChange({ [property]: urlOptions });
- }
- };
-
- return (
- <div className="search-navigator-facet-footer projects-facet-footer">
- <Select
- aria-label={translate('projects.facets.search', property)}
- className="input-super-large"
- controlShouldRenderValue={false}
- isLoading={props.isLoading}
- onChange={handleOptionChange}
- onInputChange={props.onInputChange}
- onMenuOpen={props.onOpen}
- options={props.options}
- placeholder={translate('search_verb')}
- />
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.tsx
deleted file mode 100644
index 465623eb143..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
- option?: { name: string };
- optionKey: string;
-}
-
-export default function SearchableFilterOption(props: Props) {
- const optionName = props.option ? props.option.name : props.optionKey;
- return <span>{props.optionKey !== '<null>' ? optionName : translate('unknown')}</span>;
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx
index c9282922cf1..5007716afc6 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx
@@ -18,14 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import VulnerabilityIcon from '../../../components/icons/VulnerabilityIcon';
-import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import IssuesFilter from './IssuesFilter';
+import RatingFacet from './RatingFacet';
interface Props {
- className?: string;
facet?: Facet;
headerDetail?: React.ReactNode;
maxFacetValue?: number;
@@ -34,19 +31,5 @@ interface Props {
}
export default function SecurityFilter(props: Props) {
- return (
- <IssuesFilter
- {...props}
- headerDetail={
- <span className="note little-spacer-left">
- {'( '}
- <VulnerabilityIcon className="little-spacer-right" />
- {translate('metric.vulnerabilities.name')}
- {' )'}
- </span>
- }
- name="Security"
- property="security"
- />
- );
+ return <RatingFacet {...props} name="Security" property="security" />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx
index 36f69fb6c6f..bb7dcd1a0c8 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx
@@ -17,18 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { MetricsEnum, MetricsRatingBadge } from 'design-system';
import * as React from 'react';
-import SecurityHotspotIcon from '../../../components/icons/SecurityHotspotIcon';
-import Rating from '../../../components/ui/Rating';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
import { Dict, RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
+import RangeFacetBase from './RangeFacetBase';
export interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -45,30 +43,20 @@ const labels: Dict<string> = {
};
export default function SecurityReviewFilter(props: Props) {
- const { property = 'security_review' } = props;
+ const { facet, maxFacetValue, property = 'security_review', value } = props;
return (
- <Filter
- className={props.className}
- facet={props.facet}
- header={
- <FilterHeader name={translate('metric_domain.SecurityReview')}>
- <span className="note little-spacer-left">
- {'( '}
- <SecurityHotspotIcon className="little-spacer-right" />
- {translate('metric.security_hotspots.name')}
- {' )'}
- </span>
- </FilterHeader>
- }
+ <RangeFacetBase
+ facet={facet}
+ header={translate('metric_domain.SecurityReview')}
highlightUnder={1}
- maxFacetValue={props.maxFacetValue}
+ maxFacetValue={maxFacetValue}
onQueryChange={props.onQueryChange}
options={[1, 2, 3, 4, 5]}
property={property}
renderAccessibleLabel={renderAccessibleLabel}
renderOption={renderOption}
- value={props.value}
+ value={value}
/>
);
}
@@ -78,22 +66,28 @@ function renderAccessibleLabel(option: number) {
return translateWithParameters(
'projects.facets.rating_label_single_x',
translate('metric_domain.SecurityReview'),
- formatMeasure(option, 'RATING')
+ formatMeasure(option, MetricType.Rating)
);
}
return translateWithParameters(
'projects.facets.rating_label_multi_x',
translate('metric_domain.SecurityReview'),
- formatMeasure(option, 'RATING')
+ formatMeasure(option, MetricType.Rating)
);
}
-function renderOption(option: number, selected: boolean) {
+function renderOption(option: number) {
+ const ratingFormatted = formatMeasure(option, MetricType.Rating);
+
return (
- <span>
- <Rating muted={!selected} value={option} />
- <span className="spacer-left">{labels[option]}</span>
- </span>
+ <div className="sw-flex sw-items-center">
+ <MetricsRatingBadge
+ label={ratingFormatted}
+ rating={ratingFormatted as MetricsEnum}
+ size="xs"
+ />
+ <span className="sw-ml-2">{labels[option]}</span>
+ </div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.tsx
index 94ba0534e81..e287c7af948 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.tsx
@@ -17,17 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { SizeIndicator } from 'design-system';
import * as React from 'react';
-import SizeRating from '../../../components/ui/SizeRating';
import { translate } from '../../../helpers/l10n';
import { getSizeRatingAverageValue, getSizeRatingLabel } from '../../../helpers/ratings';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
+import RangeFacetBase from './RangeFacetBase';
export interface Props {
- className?: string;
facet?: Facet;
maxFacetValue?: number;
onQueryChange: (change: RawQuery) => void;
@@ -36,22 +34,21 @@ export interface Props {
}
export default function SizeFilter(props: Props) {
- const { property = 'size' } = props;
+ const { facet, maxFacetValue, property = 'size', value } = props;
return (
- <Filter
- className={props.className}
- facet={props.facet}
+ <RangeFacetBase
+ facet={facet}
getFacetValueForOption={getFacetValueForOption}
- header={<FilterHeader name={translate('metric_domain.Size')} />}
+ header={translate('metric_domain.Size')}
highlightUnder={1}
- maxFacetValue={props.maxFacetValue}
+ maxFacetValue={maxFacetValue}
onQueryChange={props.onQueryChange}
options={[1, 2, 3, 4, 5]}
property={property}
renderAccessibleLabel={renderAccessibleLabel}
renderOption={renderOption}
- value={props.value}
+ value={value}
/>
);
}
@@ -61,11 +58,11 @@ function getFacetValueForOption(facet: Facet, option: number) {
return facet[map[option - 1]];
}
-function renderOption(option: number, selected: boolean) {
+function renderOption(option: number) {
return (
- <div className="display-flex-center">
- <SizeRating muted={!selected} value={getSizeRatingAverageValue(option)} />
- <span className="spacer-left">{getSizeRatingLabel(option)}</span>
+ <div className="sw-flex sw-items-center">
+ <SizeIndicator value={getSizeRatingAverageValue(option)} size="xs" />
+ <span className="sw-ml-2">{getSizeRatingLabel(option)}</span>
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.tsx
index 39c4b64816c..0ebbc7ce537 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.tsx
@@ -17,53 +17,38 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { debounce, difference, size, sortBy } from 'lodash';
+import { size } from 'lodash';
import * as React from 'react';
import { searchProjectTags } from '../../../api/components';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { ListStyleFacet } from '../../../apps/issues/sidebar/ListStyleFacet';
+import { translate } from '../../../helpers/l10n';
+import { highlightTerm } from '../../../helpers/search';
import { Dict, RawQuery } from '../../../types/types';
import { Facet } from '../types';
-import Filter from './Filter';
-import FilterHeader from './FilterHeader';
-import SearchableFilterFooter from './SearchableFilterFooter';
-import SearchableFilterOption from './SearchableFilterOption';
interface Props {
facet?: Facet;
- maxFacetValue?: number;
+ loadSearchResultCount: (property: string, values: string[]) => Promise<Dict<number>>;
onQueryChange: (change: RawQuery) => void;
- property?: string;
query: Dict<any>;
value?: string[];
}
interface State {
isLoading: boolean;
- search: string;
- tags: string[];
}
const LIST_SIZE = 10;
+const SEARCH_SIZE = 100;
-function renderAccessibleLabel(option: string) {
- return translateWithParameters(
- 'projects.facets.label_text_x',
- translate('projects.facets.tags'),
- option
- );
-}
-
-export default class TagsFilter extends React.PureComponent<Props, State> {
+export default class TagsFacet extends React.PureComponent<Props, State> {
mounted = false;
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
- search: '',
- tags: [],
};
- this.handleSearch = debounce(this.handleSearch, 250);
}
componentDidMount() {
@@ -74,62 +59,48 @@ export default class TagsFilter extends React.PureComponent<Props, State> {
this.mounted = false;
}
- getSearchOptions = () => {
- let tagsCopy = [...this.state.tags];
- if (this.props.facet) {
- tagsCopy = difference(tagsCopy, Object.keys(this.props.facet));
- }
- return tagsCopy.slice(0, LIST_SIZE).map((tag) => ({ label: tag, value: tag }));
- };
+ handleSearch = (search = ''): Promise<{ maxResults: boolean; results: string[] }> => {
+ this.setState({ isLoading: true });
- handleSearch = (search?: string) => {
- if (search !== this.state.search) {
- search = search || '';
- this.setState({ search, isLoading: true });
- searchProjectTags({
- q: search,
- ps: size(this.props.facet || {}) + LIST_SIZE,
- }).then(
- (result) => {
- if (this.mounted) {
- this.setState({ isLoading: false, tags: result.tags });
- }
- },
- () => {}
- );
- }
+ return searchProjectTags({
+ q: search,
+ ps: size(this.props.facet ?? {}) + LIST_SIZE,
+ }).then((result) => {
+ if (this.mounted) {
+ this.setState({ isLoading: false });
+ }
+ return { maxResults: result.tags.length === SEARCH_SIZE, results: result.tags };
+ });
};
- getSortedOptions = (facet: Facet = {}) =>
- sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]);
+ handleChange = (newValue: Dict<string[]>) => {
+ const { tags } = newValue;
+ this.props.onQueryChange({ tags: tags.join(',') });
+ };
- renderOption = (option: string) => <SearchableFilterOption optionKey={option} />;
+ loadSearchResultCount = (tags: string[]) => {
+ return this.props.loadSearchResultCount('tags', tags);
+ };
render() {
- const { property = 'tags' } = this.props;
+ const { facet, query, value = [] } = this.props;
+ const { isLoading } = this.state;
return (
- <Filter
- facet={this.props.facet}
- footer={
- <SearchableFilterFooter
- isLoading={this.state.isLoading}
- onInputChange={this.handleSearch}
- onOpen={this.handleSearch}
- onQueryChange={this.props.onQueryChange}
- options={this.getSearchOptions()}
- property={property}
- query={this.props.query}
- />
- }
- header={<FilterHeader name={translate('projects.facets.tags')} />}
- maxFacetValue={this.props.maxFacetValue}
- onQueryChange={this.props.onQueryChange}
- options={this.getSortedOptions(this.props.facet)}
- property={property}
- renderAccessibleLabel={renderAccessibleLabel}
- renderOption={this.renderOption}
- value={this.props.value}
+ <ListStyleFacet<string>
+ facetHeader={translate('projects.facets.tags')}
+ fetching={isLoading}
+ loadSearchResultCount={this.loadSearchResultCount}
+ onChange={this.handleChange}
+ onSearch={this.handleSearch}
+ query={query}
+ open
+ property="tags"
+ renderSearchResult={highlightTerm}
+ searchPlaceholder={translate('search.search_for_tags')}
+ showStatBar
+ stats={facet}
+ values={value}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
index 64cdf076954..5a1798e26a1 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
@@ -17,22 +17,50 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { FCProps } from '../../../../helpers/testUtils';
import CoverageFilter from '../CoverageFilter';
-it('renders', () => {
- const wrapper = shallow(<CoverageFilter onQueryChange={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
+it('renders options', () => {
+ renderCoverageFilter({ value: 2 });
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot();
+ expect(screen.getByRole('checkbox', { name: '≥ 80% 1' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: '70% - 80% 0' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: '50% - 70% 6' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: '30% - 50% 2' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: '< 30% 0' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'no_data 4' })).toBeInTheDocument();
+});
+
+it('updates the filter query', async () => {
+ const user = userEvent.setup();
+
+ const onQueryChange = jest.fn();
- const getFacetValueForOption = wrapper.prop('getFacetValueForOption');
- expect(
- getFacetValueForOption(
- { '80.0-*': 1, '70.0-80.0': 42, '50.0-70.0': 14, '30.0-50.0': 13, '*-30.0': 8, NO_DATA: 3 },
- 2
- )
- ).toBe(42);
+ renderCoverageFilter({ onQueryChange });
+
+ await user.click(screen.getByRole('checkbox', { name: '50% - 70% 6' }));
+
+ expect(onQueryChange).toHaveBeenCalledWith({ coverage: '3' });
});
+
+function renderCoverageFilter(props: Partial<FCProps<typeof CoverageFilter>> = {}) {
+ renderComponent(
+ <CoverageFilter
+ maxFacetValue={9}
+ onQueryChange={jest.fn()}
+ facet={{
+ '80.0-*': 1,
+ '70.0-80.0': 0,
+ '50.0-70.0': 6,
+ '30.0-50.0': 2,
+ '*-30.0': 0,
+ NO_DATA: 4,
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/DuplicationsFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/DuplicationsFilter-test.tsx
deleted file mode 100644
index 106b90aef5e..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/DuplicationsFilter-test.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import DuplicationsFilter from '../DuplicationsFilter';
-
-it('renders', () => {
- const wrapper = shallow(<DuplicationsFilter onQueryChange={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
-
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot();
- expect(renderOption(6, true)).toMatchSnapshot();
-
- const getFacetValueForOption = wrapper.prop('getFacetValueForOption');
- expect(
- getFacetValueForOption(
- { '*-3.0': 1, '3.0-5.0': 42, '5.0-10.0': 14, '10.0-20.0': 13, '20.0-*': 8, NO_DATA: 3 },
- 2
- )
- ).toBe(42);
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx
deleted file mode 100644
index 48ebf0c37c0..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockEvent } from '../../../../helpers/testUtils';
-import Filter from '../Filter';
-
-it('renders', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('renders header and footer', () => {
- expect(shallowRender({ header: <header />, footer: <footer /> })).toMatchSnapshot();
-});
-
-it('renders no results', () => {
- expect(shallowRender({ options: [] })).toMatchSnapshot();
-});
-
-it('highlights under', () => {
- expect(shallowRender({ highlightUnder: 1 })).toMatchSnapshot();
-});
-
-it('renders selected', () => {
- expect(shallowRender({ value: 2 })).toMatchSnapshot();
-});
-
-it('hightlights under selected', () => {
- expect(shallowRender({ highlightUnder: 1, value: 2 })).toMatchSnapshot();
-});
-
-it('renders multiple selected', () => {
- expect(shallowRender({ value: [1, 2] })).toMatchSnapshot();
-});
-
-it('renders facet bar chart', () => {
- expect(
- shallowRender({
- getFacetValueForOption: (facet: any, option: any) => facet[option],
- facet: { a: 17, b: 15, c: 24 },
- maxFacetValue: 24,
- options: ['a', 'b', 'c'],
- })
- ).toMatchSnapshot();
-});
-
-it('should handle click when value is single', () => {
- const onQueryChange = jest.fn();
- const wrapper = shallowRender({ onQueryChange, value: 'option1' });
-
- // select
- wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option2' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: 'option2' });
-
- onQueryChange.mockClear();
-
- // deselect
- wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option1' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: null });
-});
-
-it('should handle click when value is array', () => {
- const onQueryChange = jest.fn();
- const wrapper = shallowRender({ onQueryChange, value: ['option1', 'option2'] });
-
- // select one
- wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option2' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: 'option2' });
-
- onQueryChange.mockClear();
-
- // select other
- wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option3' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: 'option3' });
-
- onQueryChange.mockClear();
-
- // select additional
- wrapper
- .instance()
- .handleClick(mockEvent({ ctrlKey: true, currentTarget: { dataset: { key: 'option3' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: 'option1,option2,option3' });
-
- onQueryChange.mockClear();
-
- // deselect one
- wrapper
- .instance()
- .handleClick(mockEvent({ metaKey: true, currentTarget: { dataset: { key: 'option2' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: 'option1' });
-});
-
-it('should handle click when value is array with one value', () => {
- const onQueryChange = jest.fn();
- const wrapper = shallowRender({ onQueryChange, value: ['option1'] });
-
- // deselect one
- wrapper
- .instance()
- .handleClick(mockEvent({ ctrlKey: true, currentTarget: { dataset: { key: 'option1' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: null });
-
- onQueryChange.mockClear();
-
- // deselect one
- wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option1' } } }));
- expect(onQueryChange).toHaveBeenCalledWith({ foo: null });
-});
-
-function shallowRender(overrides: Partial<Filter['props']> = {}) {
- return shallow<Filter>(
- <Filter
- onQueryChange={jest.fn()}
- options={[1, 2, 3]}
- property="foo"
- renderOption={(option) => option}
- renderAccessibleLabel={(option) => option.toString()}
- {...overrides}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/FilterHeader-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/FilterHeader-test.tsx
deleted file mode 100644
index 1bce2166291..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/FilterHeader-test.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import FilterHeader from '../FilterHeader';
-
-it('renders', () => {
- expect(shallow(<FilterHeader name="foo" />)).toMatchSnapshot();
-});
-
-it('renders with children', () => {
- expect(
- shallow(
- <FilterHeader name="foo">
- <div />
- </FilterHeader>
- )
- ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx
deleted file mode 100644
index 06098410f12..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import IssuesFilter from '../IssuesFilter';
-
-it('renders', () => {
- const wrapper = shallow(<IssuesFilter name="bugs" onQueryChange={jest.fn()} property="bugs" />);
- expect(wrapper).toMatchSnapshot();
-
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
index bc3372d2163..80fdaf6143a 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
@@ -17,43 +17,64 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { FCProps } from '../../../../helpers/testUtils';
import { LanguagesFilter } from '../LanguagesFilter';
-const languages = {
- java: { key: 'java', name: 'Java' },
- cs: { key: 'cs', name: 'C#' },
- js: { key: 'js', name: 'JavaScript' },
- flex: { key: 'flex', name: 'Flex' },
- php: { key: 'php', name: 'PHP' },
- py: { key: 'py', name: 'Python' },
-};
+it('renders language names', () => {
+ renderLanguagesFilter();
+ expect(screen.getByText('Javascript')).toBeInTheDocument();
+ expect(screen.getByText('ts')).toBeInTheDocument();
+ expect(screen.getByText('Java')).toBeInTheDocument();
+ expect(screen.getByText('xml')).toBeInTheDocument();
+ expect(screen.getByText('unknown')).toBeInTheDocument();
+});
-const languagesFacet = { java: 39, cs: 4, js: 1 };
+it('filters options', async () => {
+ const user = userEvent.setup();
-it('should render the languages without the ones in the facet', () => {
- const wrapper = shallow(
- <LanguagesFilter
- facet={languagesFacet}
- languages={languages}
- onQueryChange={jest.fn()}
- query={{ languages: null }}
- />
- );
- expect(wrapper).toMatchSnapshot();
+ renderLanguagesFilter();
+
+ await user.click(screen.getByLabelText('search_verb'));
+
+ await user.keyboard('ja');
+
+ expect(screen.getByTitle('Javascript')).toBeInTheDocument();
+ expect(screen.queryByTitle('ts')).not.toBeInTheDocument();
+ expect(screen.getByTitle('Java')).toBeInTheDocument();
+ expect(screen.queryByTitle('xml')).not.toBeInTheDocument();
+ expect(screen.queryByTitle('unknown')).not.toBeInTheDocument();
});
-it('should render the languages facet with the selected languages', () => {
- const wrapper = shallow(
+it('updates the filter query', async () => {
+ const user = userEvent.setup();
+
+ const onQueryChange = jest.fn();
+
+ renderLanguagesFilter({ onQueryChange });
+
+ await user.click(screen.getByText('Java'));
+
+ expect(onQueryChange).toHaveBeenCalledWith({ languages: 'java' });
+});
+
+function renderLanguagesFilter(props: Partial<FCProps<typeof LanguagesFilter>> = {}) {
+ renderComponent(
<LanguagesFilter
- facet={languagesFacet}
- languages={languages}
+ languages={{
+ js: { name: 'Javascript', key: 'js' },
+ java: { name: 'Java', key: 'java' },
+ xml: { name: '', key: 'xml' },
+ }}
+ loadSearchResultCount={jest.fn()}
onQueryChange={jest.fn()}
- query={{ languages: ['java', 'cs'] }}
- value={['java', 'cs']}
+ query={{}}
+ facet={{ js: 12, ts: 7, java: 4, xml: 1, '<null>': 1 }}
+ value={['js', 'ts']}
+ {...props}
/>
);
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('Filter').shallow()).toMatchSnapshot();
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/MaintainabilityFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/MaintainabilityFilter-test.tsx
deleted file mode 100644
index f1356091354..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/MaintainabilityFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import MaintainabilityFilter from '../MaintainabilityFilter';
-
-it('renders', () => {
- expect(shallow(<MaintainabilityFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewCoverageFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewCoverageFilter-test.tsx
deleted file mode 100644
index abade8cae61..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewCoverageFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import NewCoverageFilter from '../NewCoverageFilter';
-
-it('renders', () => {
- expect(shallow(<NewCoverageFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewDuplicationsFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewDuplicationsFilter-test.tsx
deleted file mode 100644
index 2167c79da5c..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewDuplicationsFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import NewDuplicationsFilter from '../NewDuplicationsFilter';
-
-it('renders', () => {
- expect(shallow(<NewDuplicationsFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewLinesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewLinesFilter-test.tsx
deleted file mode 100644
index 89ca734d94e..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewLinesFilter-test.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import NewLinesFilter from '../NewLinesFilter';
-
-it('renders', () => {
- const wrapper = shallow(<NewLinesFilter onQueryChange={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
-
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot();
-
- const getFacetValueForOption = wrapper.prop('getFacetValueForOption');
- expect(
- getFacetValueForOption(
- {
- '*-1000.0': 1,
- '1000.0-10000.0': 2,
- '10000.0-100000.0': 3,
- '100000.0-500000.0': 4,
- '500000.0-*': 5,
- },
- 2
- )
- ).toBe(2);
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewMaintainabilityFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewMaintainabilityFilter-test.tsx
deleted file mode 100644
index 518f429e6cd..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewMaintainabilityFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import NewMaintainabilityFilter from '../NewMaintainabilityFilter';
-
-it('renders', () => {
- expect(shallow(<NewMaintainabilityFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewReliabilityFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewReliabilityFilter-test.tsx
deleted file mode 100644
index 3148cc1717b..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewReliabilityFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import NewReliabilityFilter from '../NewReliabilityFilter';
-
-it('renders', () => {
- expect(shallow(<NewReliabilityFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewSecurityFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewSecurityFilter-test.tsx
deleted file mode 100644
index 8ea1ade4f51..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/NewSecurityFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import NewSecurityFilter from '../NewSecurityFilter';
-
-it('renders', () => {
- expect(shallow(<NewSecurityFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualifierFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualifierFilter-test.tsx
deleted file mode 100644
index 2a8ff540519..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualifierFilter-test.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { ComponentQualifier } from '../../../../types/component';
-import Filter from '../Filter';
-import QualifierFilter, { QualifierFilterProps } from '../QualifierFilter';
-
-it('renders', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
-
- const { renderOption } = wrapper.find(Filter).props();
- expect(renderOption(ComponentQualifier.Application, false)).toMatchSnapshot(
- 'renderOption result'
- );
-});
-
-function shallowRender(props: Partial<QualifierFilterProps> = {}) {
- return shallow(<QualifierFilter onQueryChange={jest.fn()} value={undefined} {...props} />);
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
index 279dfbb1142..699da7601bf 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
@@ -17,26 +17,53 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import QualityGateFilter, { Props } from '../QualityGateFilter';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { FCProps } from '../../../../helpers/testUtils';
+import QualityGateFacet from '../QualityGateFilter';
-it('renders', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
+it('renders options', () => {
+ renderQualityGateFilter();
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot();
+ expect(screen.getByRole('checkbox', { name: 'metric.level.OK 6' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'metric.level.ERROR 3' })).toBeInTheDocument();
});
-it('should render with warning facet', () => {
- expect(
- shallowRender({ facet: { ERROR: 1, WARN: 2, OK: 3 } })
- .find('Filter')
- .prop('options')
- ).toEqual(['OK', 'WARN', 'ERROR']);
+it('updates the filter query', async () => {
+ const user = userEvent.setup();
+
+ const onQueryChange = jest.fn();
+
+ renderQualityGateFilter({ onQueryChange });
+
+ await user.click(screen.getByRole('checkbox', { name: 'metric.level.OK 6' }));
+
+ expect(onQueryChange).toHaveBeenCalledWith({ gate: 'OK' });
+});
+
+it('handles multiselection', async () => {
+ const user = userEvent.setup();
+
+ const onQueryChange = jest.fn();
+
+ renderQualityGateFilter({ onQueryChange, value: ['OK'] });
+
+ await user.keyboard('{Control>}');
+ await user.click(screen.getByRole('checkbox', { name: 'metric.level.ERROR 3' }));
+ await user.keyboard('{/Control}');
+
+ expect(onQueryChange).toHaveBeenCalledWith({ gate: 'OK,ERROR' });
});
-function shallowRender(props: Partial<Props> = {}) {
- return shallow(<QualityGateFilter onQueryChange={jest.fn()} {...props} />);
+function renderQualityGateFilter(props: Partial<FCProps<typeof QualityGateFacet>> = {}) {
+ renderComponent(
+ <QualityGateFacet
+ maxFacetValue={9}
+ onQueryChange={jest.fn()}
+ facet={{ OK: 6, ERROR: 3 }}
+ {...props}
+ />
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/ReliabilityFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/ReliabilityFilter-test.tsx
deleted file mode 100644
index 109355a6ed4..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/ReliabilityFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import ReliabilityFilter from '../ReliabilityFilter';
-
-it('renders', () => {
- expect(shallow(<ReliabilityFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.tsx
deleted file mode 100644
index a4190c94c74..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import Select from '../../../../components/controls/Select';
-import SearchableFilterFooter from '../SearchableFilterFooter';
-
-const options = [
- { label: 'java', value: 'java' },
- { label: 'js', value: 'js' },
- { label: 'csharp', value: 'csharp' },
-];
-
-it('should render items without the ones in the facet', () => {
- const wrapper = shallow(
- <SearchableFilterFooter
- onQueryChange={jest.fn()}
- options={options}
- property="languages"
- query={{ languages: ['java'] }}
- />
- );
- expect(wrapper.find(Select).props().options).toMatchSnapshot();
-});
-
-it('should properly handle a change of the facet value', () => {
- const onQueryChange = jest.fn();
- const wrapper = shallow(
- <SearchableFilterFooter
- onQueryChange={onQueryChange}
- options={options}
- property="languages"
- query={{ languages: ['java'] }}
- />
- );
- wrapper.find(Select).simulate('change', { value: 'js' });
- expect(onQueryChange).toHaveBeenCalledWith({ languages: 'java,js' });
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterOption-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterOption-test.tsx
deleted file mode 100644
index e1b6a8d6c36..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterOption-test.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import SearchableFilterOption from '../SearchableFilterOption';
-
-it('renders', () => {
- expect(shallow(<SearchableFilterOption optionKey="foo" />)).toMatchSnapshot();
- expect(
- shallow(<SearchableFilterOption option={{ name: 'bar' }} optionKey="foo" />)
- ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityFilter-test.tsx
deleted file mode 100644
index ea0c75764c4..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityFilter-test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import SecurityFilter from '../SecurityFilter';
-
-it('renders', () => {
- expect(shallow(<SecurityFilter onQueryChange={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityReviewFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityReviewFilter-test.tsx
deleted file mode 100644
index 4a729ecd2f0..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SecurityReviewFilter-test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import SecurityReviewFilter from '../SecurityReviewFilter';
-
-it('renders', () => {
- const wrapper = shallow(<SecurityReviewFilter onQueryChange={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
-
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot('option');
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SizeFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SizeFilter-test.tsx
deleted file mode 100644
index 0368b9fd036..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SizeFilter-test.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import SizeFilter from '../SizeFilter';
-
-it('renders', () => {
- const wrapper = shallow(<SizeFilter onQueryChange={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
-
- const renderOption = wrapper.prop('renderOption');
- expect(renderOption(2, false)).toMatchSnapshot();
-
- const getFacetValueForOption = wrapper.prop('getFacetValueForOption');
- expect(
- getFacetValueForOption(
- {
- '*-1000.0': 1,
- '1000.0-10000.0': 42,
- '10000.0-100000.0': 14,
- '100000.0-500000.0': 13,
- '500000.0-*': 8,
- },
- 2
- )
- ).toBe(42);
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.tsx
index e0a637de2a6..e386f20c6d8 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.tsx
@@ -17,44 +17,77 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import TagsFilter from '../TagsFilter';
+import { searchProjectTags } from '../../../../api/components';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import TagsFacet from '../TagsFilter';
-const tags = ['lang', 'sonar', 'csharp', 'dotnet', 'it', 'net'];
-const tagsFacet = { lang: 4, sonar: 3, csharp: 1 };
+jest.mock('../../../../api/components', () => ({
+ searchProjectTags: jest.fn(),
+}));
-it('should render the tags without the ones in the facet', () => {
- const wrapper = shallow(
- <TagsFilter facet={tagsFacet} onQueryChange={jest.fn()} query={{ tags: null }} />
- );
- expect(wrapper).toMatchSnapshot();
- wrapper.setState({ tags });
- expect(wrapper).toMatchSnapshot();
+it('renders language names', () => {
+ renderTagsFacet();
+ expect(screen.getByText('style')).toBeInTheDocument();
+ expect(screen.getByText('custom1')).toBeInTheDocument();
+ expect(screen.getByText('cheese')).toBeInTheDocument();
});
-it('should render the tags facet with the selected tags', () => {
- const wrapper = shallow(
- <TagsFilter
- facet={tagsFacet}
- onQueryChange={jest.fn()}
- query={{ tags: ['lang', 'sonar'] }}
- value={['lang', 'sonar']}
- />
- );
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('Filter').shallow()).toMatchSnapshot();
+it('filters options', async () => {
+ const user = userEvent.setup();
+
+ jest.mocked(searchProjectTags).mockResolvedValueOnce({ tags: ['style', 'stunt'] });
+ const loadSearchResultCount = jest.fn().mockResolvedValueOnce({ style: 3, stunt: 0 });
+
+ renderTagsFacet({ loadSearchResultCount });
+
+ await user.click(screen.getByLabelText('search_verb'));
+
+ await user.keyboard('st');
+
+ expect(screen.getByTitle('style')).toBeInTheDocument();
+ expect(screen.getByTitle('stunt')).toBeInTheDocument();
+ expect(screen.queryByTitle('cheese')).not.toBeInTheDocument();
+ expect(screen.queryByTitle('custom1')).not.toBeInTheDocument();
+});
+
+it('updates the filter query', async () => {
+ const user = userEvent.setup();
+
+ const onQueryChange = jest.fn();
+
+ renderTagsFacet({ onQueryChange });
+
+ await user.click(screen.getByText('style'));
+
+ expect(onQueryChange).toHaveBeenCalledWith({ tags: 'style' });
});
-it('should render maximum 10 tags in the searchbox results', () => {
- const wrapper = shallow(
- <TagsFilter
- facet={{ ...tagsFacet, ad: 1 }}
+it('handles multiselection', async () => {
+ const user = userEvent.setup();
+
+ const onQueryChange = jest.fn();
+
+ renderTagsFacet({ onQueryChange });
+
+ await user.keyboard('{Control>}');
+ await user.click(screen.getByText('style'));
+ await user.keyboard('{/Control}');
+
+ expect(onQueryChange).toHaveBeenCalledWith({ tags: 'custom1,style' });
+});
+
+function renderTagsFacet(props: Partial<TagsFacet['props']> = {}) {
+ renderComponent(
+ <TagsFacet
+ loadSearchResultCount={jest.fn()}
onQueryChange={jest.fn()}
- query={{ languages: ['java', 'ad'] }}
- value={['java', 'ad']}
+ query={{}}
+ facet={{ cheese: 5, style: 3, custom1: 1 }}
+ value={['custom1']}
+ {...props}
/>
);
- wrapper.setState({ tags: [...tags, 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'ag', 'ah', 'ai'] });
- expect(wrapper).toMatchSnapshot();
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap
deleted file mode 100644
index b6cdba64d87..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap
+++ /dev/null
@@ -1,44 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- getFacetValueForOption={[Function]}
- header={
- <FilterHeader
- name="metric_domain.Coverage"
- />
- }
- highlightUnder={1}
- highlightUnderMax={5}
- onQueryChange={[MockFunction]}
- options={
- [
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- ]
- }
- property="coverage"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders 2`] = `
-<div
- className="display-flex-center"
->
- <CoverageRating
- muted={true}
- value={75}
- />
- <span
- className="spacer-left"
- >
- 70% - 80%
- </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/DuplicationsFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/DuplicationsFilter-test.tsx.snap
deleted file mode 100644
index 9d0dd8b76a3..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/DuplicationsFilter-test.tsx.snap
+++ /dev/null
@@ -1,61 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- getFacetValueForOption={[Function]}
- header={
- <FilterHeader
- name="metric_domain.Duplications"
- />
- }
- highlightUnder={1}
- highlightUnderMax={5}
- onQueryChange={[MockFunction]}
- options={
- [
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- ]
- }
- property="duplications"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders 2`] = `
-<div
- className="display-flex-center"
->
- <DuplicationsRating
- muted={true}
- size="small"
- value={4}
- />
- <span
- className="spacer-left"
- >
- 3% - 5%
- </span>
-</div>
-`;
-
-exports[`renders 3`] = `
-<div
- className="display-flex-center"
->
- <span
- className="spacer-left"
- >
- <span
- className="big-spacer-left"
- >
- no_data
- </span>
- </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/Filter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/Filter-test.tsx.snap
deleted file mode 100644
index a9b455d7bcf..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/Filter-test.tsx.snap
+++ /dev/null
@@ -1,580 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`highlights under 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- key="1"
- >
- <button
- aria-checked={false}
- aria-label="1"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={1}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 1
- </span>
- </button>
- </li>
- <li
- className="search-navigator-facet-worse-than-highlight"
- key="2"
- >
- <button
- aria-checked={false}
- aria-label="2"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={2}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 2
- </span>
- </button>
- </li>
- <li
- className="search-navigator-facet-worse-than-highlight last"
- key="3"
- >
- <button
- aria-checked={false}
- aria-label="3"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={3}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 3
- </span>
- </button>
- </li>
- </ul>
-</div>
-`;
-
-exports[`hightlights under selected 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- key="1"
- >
- <button
- aria-checked={false}
- aria-label="1"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={1}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 1
- </span>
- </button>
- </li>
- <li
- className="search-navigator-facet-worse-than-highlight active"
- key="2"
- >
- <button
- aria-checked={true}
- aria-label="2"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key={2}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 2
- </span>
- </button>
- </li>
- <li
- className="search-navigator-facet-worse-than-highlight last"
- key="3"
- >
- <button
- aria-checked={true}
- aria-label="3"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={3}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 3
- </span>
- </button>
- </li>
- </ul>
-</div>
-`;
-
-exports[`renders 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- key="1"
- >
- <button
- aria-checked={false}
- aria-label="1"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={1}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 1
- </span>
- </button>
- </li>
- <li
- key="2"
- >
- <button
- aria-checked={false}
- aria-label="2"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={2}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 2
- </span>
- </button>
- </li>
- <li
- key="3"
- >
- <button
- aria-checked={false}
- aria-label="3"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={3}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 3
- </span>
- </button>
- </li>
- </ul>
-</div>
-`;
-
-exports[`renders facet bar chart 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- key="a"
- >
- <button
- aria-checked={false}
- aria-label="a"
- className="facet search-navigator-facet projects-facet button-link"
- data-key="a"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- a
- </span>
- <span
- className="facet-stat"
- >
- 17
- <div
- className="projects-facet-bar"
- >
- <div
- className="projects-facet-bar-inner"
- style={
- {
- "width": 42.5,
- }
- }
- />
- </div>
- </span>
- </button>
- </li>
- <li
- key="b"
- >
- <button
- aria-checked={false}
- aria-label="b"
- className="facet search-navigator-facet projects-facet button-link"
- data-key="b"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- b
- </span>
- <span
- className="facet-stat"
- >
- 15
- <div
- className="projects-facet-bar"
- >
- <div
- className="projects-facet-bar-inner"
- style={
- {
- "width": 37.5,
- }
- }
- />
- </div>
- </span>
- </button>
- </li>
- <li
- key="c"
- >
- <button
- aria-checked={false}
- aria-label="c"
- className="facet search-navigator-facet projects-facet button-link"
- data-key="c"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- c
- </span>
- <span
- className="facet-stat"
- >
- 24
- <div
- className="projects-facet-bar"
- >
- <div
- className="projects-facet-bar-inner"
- style={
- {
- "width": 60,
- }
- }
- />
- </div>
- </span>
- </button>
- </li>
- </ul>
-</div>
-`;
-
-exports[`renders header and footer 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <header />
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- key="1"
- >
- <button
- aria-checked={false}
- aria-label="1"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={1}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 1
- </span>
- </button>
- </li>
- <li
- key="2"
- >
- <button
- aria-checked={false}
- aria-label="2"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={2}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 2
- </span>
- </button>
- </li>
- <li
- key="3"
- >
- <button
- aria-checked={false}
- aria-label="3"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={3}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 3
- </span>
- </button>
- </li>
- </ul>
- <footer />
-</div>
-`;
-
-exports[`renders multiple selected 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- className="active"
- key="1"
- >
- <button
- aria-checked={true}
- aria-label="1"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key={1}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 1
- </span>
- </button>
- </li>
- <li
- className="active"
- key="2"
- >
- <button
- aria-checked={true}
- aria-label="2"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key={2}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 2
- </span>
- </button>
- </li>
- <li
- key="3"
- >
- <button
- aria-checked={false}
- aria-label="3"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={3}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 3
- </span>
- </button>
- </li>
- </ul>
-</div>
-`;
-
-exports[`renders no results 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <div
- className="search-navigator-facet-empty"
- >
- <em>
- projects.facets.no_available_filters_clear_others
- </em>
- </div>
-</div>
-`;
-
-exports[`renders selected 1`] = `
-<div
- className="search-navigator-facet-box"
- data-key="foo"
->
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- key="1"
- >
- <button
- aria-checked={false}
- aria-label="1"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={1}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 1
- </span>
- </button>
- </li>
- <li
- className="active"
- key="2"
- >
- <button
- aria-checked={true}
- aria-label="2"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key={2}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 2
- </span>
- </button>
- </li>
- <li
- key="3"
- >
- <button
- aria-checked={false}
- aria-label="3"
- className="facet search-navigator-facet projects-facet button-link"
- data-key={3}
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- 3
- </span>
- </button>
- </li>
- </ul>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/FilterHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/FilterHeader-test.tsx.snap
deleted file mode 100644
index 95be013ebaa..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/FilterHeader-test.tsx.snap
+++ /dev/null
@@ -1,26 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
- className="search-navigator-facet-header projects-facet-header"
->
- <h3
- className="h4"
- >
- foo
- </h3>
-</div>
-`;
-
-exports[`renders with children 1`] = `
-<div
- className="search-navigator-facet-header projects-facet-header"
->
- <h3
- className="h4"
- >
- foo
- </h3>
- <div />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/IssuesFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/IssuesFilter-test.tsx.snap
deleted file mode 100644
index f21d8b13b45..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/IssuesFilter-test.tsx.snap
+++ /dev/null
@@ -1,39 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- header={
- <FilterHeader
- name="metric_domain.bugs"
- />
- }
- highlightUnder={1}
- onQueryChange={[MockFunction]}
- options={
- [
- 1,
- 2,
- 3,
- 4,
- 5,
- ]
- }
- property="bugs"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders 2`] = `
-<span>
- <Rating
- muted={true}
- value={2}
- />
- <span
- className="spacer-left"
- >
- projects.facets.rating_x.B
- </span>
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.tsx.snap
deleted file mode 100644
index e0de880f2c5..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.tsx.snap
+++ /dev/null
@@ -1,264 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render the languages facet with the selected languages 1`] = `
-<Filter
- facet={
- {
- "cs": 4,
- "java": 39,
- "js": 1,
- }
- }
- footer={
- <SearchableFilterFooter
- onQueryChange={[MockFunction]}
- options={
- [
- {
- "label": "Flex",
- "value": "flex",
- },
- {
- "label": "PHP",
- "value": "php",
- },
- {
- "label": "Python",
- "value": "py",
- },
- ]
- }
- property="languages"
- query={
- {
- "languages": [
- "java",
- "cs",
- ],
- }
- }
- />
- }
- header={
- <FilterHeader
- name="projects.facets.languages"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "java",
- "cs",
- "js",
- ]
- }
- property="languages"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
- value={
- [
- "java",
- "cs",
- ]
- }
-/>
-`;
-
-exports[`should render the languages facet with the selected languages 2`] = `
-<div
- className="search-navigator-facet-box"
- data-key="languages"
->
- <FilterHeader
- name="projects.facets.languages"
- />
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- className="active"
- key="java"
- >
- <button
- aria-checked={true}
- aria-label="projects.facets.label_text_x.projects.facets.languages.Java"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key="java"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- <SearchableFilterOption
- option={
- {
- "key": "java",
- "name": "Java",
- }
- }
- optionKey="java"
- />
- </span>
- <span
- className="facet-stat"
- >
- 39
- </span>
- </button>
- </li>
- <li
- className="active"
- key="cs"
- >
- <button
- aria-checked={true}
- aria-label="projects.facets.label_text_x.projects.facets.languages.C#"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key="cs"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- <SearchableFilterOption
- option={
- {
- "key": "cs",
- "name": "C#",
- }
- }
- optionKey="cs"
- />
- </span>
- <span
- className="facet-stat"
- >
- 4
- </span>
- </button>
- </li>
- <li
- key="js"
- >
- <button
- aria-checked={false}
- aria-label="projects.facets.label_text_x.projects.facets.languages.JavaScript"
- className="facet search-navigator-facet projects-facet button-link"
- data-key="js"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- <SearchableFilterOption
- option={
- {
- "key": "js",
- "name": "JavaScript",
- }
- }
- optionKey="js"
- />
- </span>
- <span
- className="facet-stat"
- >
- 1
- </span>
- </button>
- </li>
- </ul>
- <SearchableFilterFooter
- onQueryChange={[MockFunction]}
- options={
- [
- {
- "label": "Flex",
- "value": "flex",
- },
- {
- "label": "PHP",
- "value": "php",
- },
- {
- "label": "Python",
- "value": "py",
- },
- ]
- }
- property="languages"
- query={
- {
- "languages": [
- "java",
- "cs",
- ],
- }
- }
- />
-</div>
-`;
-
-exports[`should render the languages without the ones in the facet 1`] = `
-<Filter
- facet={
- {
- "cs": 4,
- "java": 39,
- "js": 1,
- }
- }
- footer={
- <SearchableFilterFooter
- onQueryChange={[MockFunction]}
- options={
- [
- {
- "label": "Flex",
- "value": "flex",
- },
- {
- "label": "PHP",
- "value": "php",
- },
- {
- "label": "Python",
- "value": "py",
- },
- ]
- }
- property="languages"
- query={
- {
- "languages": null,
- }
- }
- />
- }
- header={
- <FilterHeader
- name="projects.facets.languages"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "java",
- "cs",
- "js",
- ]
- }
- property="languages"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/MaintainabilityFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/MaintainabilityFilter-test.tsx.snap
deleted file mode 100644
index 74c863bc426..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/MaintainabilityFilter-test.tsx.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<IssuesFilter
- headerDetail={
- <span
- className="note little-spacer-left"
- >
- (
- <CodeSmellIcon
- className="little-spacer-right"
- />
- metric.code_smells.name
- )
- </span>
- }
- name="Maintainability"
- onQueryChange={[MockFunction]}
- property="maintainability"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewCoverageFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewCoverageFilter-test.tsx.snap
deleted file mode 100644
index 893586a9cfc..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewCoverageFilter-test.tsx.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<CoverageFilter
- className="leak-facet-box"
- onQueryChange={[MockFunction]}
- property="new_coverage"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewDuplicationsFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewDuplicationsFilter-test.tsx.snap
deleted file mode 100644
index 4fce695bb25..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewDuplicationsFilter-test.tsx.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<DuplicationsFilter
- className="leak-facet-box"
- onQueryChange={[MockFunction]}
- property="new_duplications"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap
deleted file mode 100644
index c7d7b0face0..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- className="leak-facet-box"
- getFacetValueForOption={[Function]}
- header={
- <FilterHeader
- name="projects.facets.new_lines"
- />
- }
- highlightUnder={1}
- onQueryChange={[MockFunction]}
- options={
- [
- 1,
- 2,
- 3,
- 4,
- 5,
- ]
- }
- property="new_lines"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders 2`] = `
-<span>
- 1k - 10k
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewMaintainabilityFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewMaintainabilityFilter-test.tsx.snap
deleted file mode 100644
index aee66f09413..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewMaintainabilityFilter-test.tsx.snap
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<IssuesFilter
- className="leak-facet-box"
- headerDetail={
- <span
- className="note little-spacer-left"
- >
- (
- <CodeSmellIcon
- className="little-spacer-right"
- />
- metric.code_smells.name
- )
- </span>
- }
- name="Maintainability"
- onQueryChange={[MockFunction]}
- property="new_maintainability"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewReliabilityFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewReliabilityFilter-test.tsx.snap
deleted file mode 100644
index c2a1184a792..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewReliabilityFilter-test.tsx.snap
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<IssuesFilter
- className="leak-facet-box"
- headerDetail={
- <span
- className="note little-spacer-left"
- >
- (
- <BugIcon
- className="little-spacer-right"
- />
- metric.bugs.name
- )
- </span>
- }
- name="Reliability"
- onQueryChange={[MockFunction]}
- property="new_reliability"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewSecurityFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewSecurityFilter-test.tsx.snap
deleted file mode 100644
index ff165f6d2c7..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewSecurityFilter-test.tsx.snap
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<IssuesFilter
- className="leak-facet-box"
- headerDetail={
- <span
- className="note little-spacer-left"
- >
- (
- <VulnerabilityIcon
- className="little-spacer-right"
- />
- metric.vulnerabilities.name
- )
- </span>
- }
- name="Security"
- onQueryChange={[MockFunction]}
- property="new_security"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualifierFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualifierFilter-test.tsx.snap
deleted file mode 100644
index 0407a0cc0d9..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualifierFilter-test.tsx.snap
+++ /dev/null
@@ -1,34 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- header={
- <FilterHeader
- name="projects.facets.qualifier"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "TRK",
- "APP",
- ]
- }
- property="qualifier"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders: renderOption result 1`] = `
-<span
- className="display-flex-center"
->
- <QualifierIcon
- className="spacer-right"
- fill="currentColor"
- qualifier="APP"
- />
- qualifier.APP
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualityGateFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualityGateFilter-test.tsx.snap
deleted file mode 100644
index 91e8ff5fadc..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/QualityGateFilter-test.tsx.snap
+++ /dev/null
@@ -1,31 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- header={
- <FilterHeader
- name="projects.facets.quality_gate"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "OK",
- "ERROR",
- ]
- }
- property="gate"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders 2`] = `
-<React.Fragment>
- <Level
- level={2}
- muted={true}
- small={true}
- />
-</React.Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/ReliabilityFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/ReliabilityFilter-test.tsx.snap
deleted file mode 100644
index 355fccc7ef6..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/ReliabilityFilter-test.tsx.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<IssuesFilter
- headerDetail={
- <span
- className="note little-spacer-left"
- >
- (
- <BugIcon
- className="little-spacer-right"
- />
- metric.bugs.name
- )
- </span>
- }
- name="Reliability"
- onQueryChange={[MockFunction]}
- property="reliability"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.tsx.snap
deleted file mode 100644
index 83c13ac4bd4..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.tsx.snap
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render items without the ones in the facet 1`] = `
-[
- {
- "label": "java",
- "value": "java",
- },
- {
- "label": "js",
- "value": "js",
- },
- {
- "label": "csharp",
- "value": "csharp",
- },
-]
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterOption-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterOption-test.tsx.snap
deleted file mode 100644
index 43960a58f7e..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterOption-test.tsx.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<span>
- foo
-</span>
-`;
-
-exports[`renders 2`] = `
-<span>
- bar
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityFilter-test.tsx.snap
deleted file mode 100644
index 9039e7ab671..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityFilter-test.tsx.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<IssuesFilter
- headerDetail={
- <span
- className="note little-spacer-left"
- >
- (
- <VulnerabilityIcon
- className="little-spacer-right"
- />
- metric.vulnerabilities.name
- )
- </span>
- }
- name="Security"
- onQueryChange={[MockFunction]}
- property="security"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityReviewFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityReviewFilter-test.tsx.snap
deleted file mode 100644
index 5e967d3fce5..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SecurityReviewFilter-test.tsx.snap
+++ /dev/null
@@ -1,50 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- header={
- <FilterHeader
- name="metric_domain.SecurityReview"
- >
- <span
- className="note little-spacer-left"
- >
- (
- <SecurityHotspotIcon
- className="little-spacer-right"
- />
- metric.security_hotspots.name
- )
- </span>
- </FilterHeader>
- }
- highlightUnder={1}
- onQueryChange={[MockFunction]}
- options={
- [
- 1,
- 2,
- 3,
- 4,
- 5,
- ]
- }
- property="security_review"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders: option 1`] = `
-<span>
- <Rating
- muted={true}
- value={2}
- />
- <span
- className="spacer-left"
- >
- 70% - 80%
- </span>
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SizeFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SizeFilter-test.tsx.snap
deleted file mode 100644
index d3e8cedaca7..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SizeFilter-test.tsx.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Filter
- getFacetValueForOption={[Function]}
- header={
- <FilterHeader
- name="metric_domain.Size"
- />
- }
- highlightUnder={1}
- onQueryChange={[MockFunction]}
- options={
- [
- 1,
- 2,
- 3,
- 4,
- 5,
- ]
- }
- property="size"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`renders 2`] = `
-<div
- className="display-flex-center"
->
- <SizeRating
- muted={true}
- value={5000}
- />
- <span
- className="spacer-left"
- >
- 1k - 10k
- </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.tsx.snap
deleted file mode 100644
index ee936ccc3b6..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.tsx.snap
+++ /dev/null
@@ -1,365 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render maximum 10 tags in the searchbox results 1`] = `
-<Filter
- facet={
- {
- "ad": 1,
- "csharp": 1,
- "lang": 4,
- "sonar": 3,
- }
- }
- footer={
- <SearchableFilterFooter
- isLoading={false}
- onInputChange={[Function]}
- onOpen={[Function]}
- onQueryChange={[MockFunction]}
- options={
- [
- {
- "label": "dotnet",
- "value": "dotnet",
- },
- {
- "label": "it",
- "value": "it",
- },
- {
- "label": "net",
- "value": "net",
- },
- {
- "label": "aa",
- "value": "aa",
- },
- {
- "label": "ab",
- "value": "ab",
- },
- {
- "label": "ac",
- "value": "ac",
- },
- {
- "label": "ae",
- "value": "ae",
- },
- {
- "label": "af",
- "value": "af",
- },
- {
- "label": "ag",
- "value": "ag",
- },
- {
- "label": "ah",
- "value": "ah",
- },
- ]
- }
- property="tags"
- query={
- {
- "languages": [
- "java",
- "ad",
- ],
- }
- }
- />
- }
- header={
- <FilterHeader
- name="projects.facets.tags"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "lang",
- "sonar",
- "ad",
- "csharp",
- ]
- }
- property="tags"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
- value={
- [
- "java",
- "ad",
- ]
- }
-/>
-`;
-
-exports[`should render the tags facet with the selected tags 1`] = `
-<Filter
- facet={
- {
- "csharp": 1,
- "lang": 4,
- "sonar": 3,
- }
- }
- footer={
- <SearchableFilterFooter
- isLoading={false}
- onInputChange={[Function]}
- onOpen={[Function]}
- onQueryChange={[MockFunction]}
- options={[]}
- property="tags"
- query={
- {
- "tags": [
- "lang",
- "sonar",
- ],
- }
- }
- />
- }
- header={
- <FilterHeader
- name="projects.facets.tags"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "lang",
- "sonar",
- "csharp",
- ]
- }
- property="tags"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
- value={
- [
- "lang",
- "sonar",
- ]
- }
-/>
-`;
-
-exports[`should render the tags facet with the selected tags 2`] = `
-<div
- className="search-navigator-facet-box"
- data-key="tags"
->
- <FilterHeader
- name="projects.facets.tags"
- />
- <ul
- className="search-navigator-facet-list projects-facet-list"
- >
- <li
- className="active"
- key="lang"
- >
- <button
- aria-checked={true}
- aria-label="projects.facets.label_text_x.projects.facets.tags.lang"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key="lang"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- <SearchableFilterOption
- optionKey="lang"
- />
- </span>
- <span
- className="facet-stat"
- >
- 4
- </span>
- </button>
- </li>
- <li
- className="active"
- key="sonar"
- >
- <button
- aria-checked={true}
- aria-label="projects.facets.label_text_x.projects.facets.tags.sonar"
- className="facet search-navigator-facet projects-facet button-link active"
- data-key="sonar"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- <SearchableFilterOption
- optionKey="sonar"
- />
- </span>
- <span
- className="facet-stat"
- >
- 3
- </span>
- </button>
- </li>
- <li
- key="csharp"
- >
- <button
- aria-checked={false}
- aria-label="projects.facets.label_text_x.projects.facets.tags.csharp"
- className="facet search-navigator-facet projects-facet button-link"
- data-key="csharp"
- onClick={[Function]}
- role="checkbox"
- tabIndex={0}
- type="button"
- >
- <span
- className="facet-name"
- >
- <SearchableFilterOption
- optionKey="csharp"
- />
- </span>
- <span
- className="facet-stat"
- >
- 1
- </span>
- </button>
- </li>
- </ul>
- <SearchableFilterFooter
- isLoading={false}
- onInputChange={[Function]}
- onOpen={[Function]}
- onQueryChange={[MockFunction]}
- options={[]}
- property="tags"
- query={
- {
- "tags": [
- "lang",
- "sonar",
- ],
- }
- }
- />
-</div>
-`;
-
-exports[`should render the tags without the ones in the facet 1`] = `
-<Filter
- facet={
- {
- "csharp": 1,
- "lang": 4,
- "sonar": 3,
- }
- }
- footer={
- <SearchableFilterFooter
- isLoading={false}
- onInputChange={[Function]}
- onOpen={[Function]}
- onQueryChange={[MockFunction]}
- options={[]}
- property="tags"
- query={
- {
- "tags": null,
- }
- }
- />
- }
- header={
- <FilterHeader
- name="projects.facets.tags"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "lang",
- "sonar",
- "csharp",
- ]
- }
- property="tags"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
-
-exports[`should render the tags without the ones in the facet 2`] = `
-<Filter
- facet={
- {
- "csharp": 1,
- "lang": 4,
- "sonar": 3,
- }
- }
- footer={
- <SearchableFilterFooter
- isLoading={false}
- onInputChange={[Function]}
- onOpen={[Function]}
- onQueryChange={[MockFunction]}
- options={
- [
- {
- "label": "dotnet",
- "value": "dotnet",
- },
- {
- "label": "it",
- "value": "it",
- },
- {
- "label": "net",
- "value": "net",
- },
- ]
- }
- property="tags"
- query={
- {
- "tags": null,
- }
- }
- />
- }
- header={
- <FilterHeader
- name="projects.facets.tags"
- />
- }
- onQueryChange={[MockFunction]}
- options={
- [
- "lang",
- "sonar",
- "csharp",
- ]
- }
- property="tags"
- renderAccessibleLabel={[Function]}
- renderOption={[Function]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts
index 235018b7a9a..5dc045f5f05 100644
--- a/server/sonar-web/src/main/js/apps/projects/utils.ts
+++ b/server/sonar-web/src/main/js/apps/projects/utils.ts
@@ -26,7 +26,7 @@ import { isDiffMetric } from '../../helpers/measures';
import { RequestData } from '../../helpers/request';
import { MetricKey } from '../../types/metrics';
import { Dict } from '../../types/types';
-import { convertToFilter, Query } from './query';
+import { Query, convertToFilter } from './query';
interface SortingOption {
class?: string;
@@ -219,7 +219,7 @@ function defineFacets(query: Query): string[] {
return FACETS;
}
-function convertToQueryData(query: Query, isFavorite: boolean, defaultData = {}) {
+export function convertToQueryData(query: Query, isFavorite: boolean, defaultData = {}) {
const data: RequestData = { ...defaultData };
const filter = convertToFilter(query, isFavorite);
const sort = convertToSorting(query);
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/ratings-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/ratings-test.ts
index 4c25a947e96..7690a7d812f 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/ratings-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/ratings-test.ts
@@ -20,7 +20,6 @@
import {
getCoverageRatingAverageValue,
getCoverageRatingLabel,
- getDuplicationsRatingAverageValue,
getDuplicationsRatingLabel,
getSizeRatingAverageValue,
getSizeRatingLabel,
@@ -72,18 +71,6 @@ describe('getDuplicationsRatingLabel', () => {
});
});
-describe('getDuplicationsRatingAverageValue', () => {
- it.each([
- [1, 1.5],
- [2, 4],
- [3, 7.5],
- [4, 15],
- [5, 30],
- ])('should return the correct value', (rating, value) => {
- expect(getDuplicationsRatingAverageValue(rating)).toBe(value);
- });
-});
-
describe('getSizeRatingLabel', () => {
it('should fail', () => {
expect(() => {
diff --git a/server/sonar-web/src/main/js/helpers/ratings.ts b/server/sonar-web/src/main/js/helpers/ratings.ts
index 6b85b84c162..05fd9c7a681 100644
--- a/server/sonar-web/src/main/js/helpers/ratings.ts
+++ b/server/sonar-web/src/main/js/helpers/ratings.ts
@@ -47,12 +47,6 @@ export function getDuplicationsRatingLabel(rating: number): string {
return mapping[rating - 1];
}
-export function getDuplicationsRatingAverageValue(rating: number): number {
- checkNumberRating(rating);
- const mapping = [1.5, 4, 7.5, 15, 30];
- return mapping[rating - 1];
-}
-
export function getSizeRatingLabel(rating: number): string {
checkNumberRating(rating);
const mapping = ['< 1k', '1k - 10k', '10k - 100k', '100k - 500k', '> 500k'];
@@ -73,3 +67,8 @@ export const getMaintainabilityGrid = (ratingGridSetting: string) => {
return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0];
};
+
+const DUPLICATION_RATINGS: ['A', 'B', 'C', 'D', 'E', 'F'] = ['A', 'B', 'C', 'D', 'E', 'F'];
+export function duplicationValueToRating(val: number) {
+ return DUPLICATION_RATINGS[val - 1];
+}