diff options
author | Jay <jeremy.davis@sonarsource.com> | 2023-07-06 17:32:55 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-07-11 20:03:24 +0000 |
commit | 76c1f065c934a9abcba93dedb97fe7b4b8277ec4 (patch) | |
tree | f0e190f826d1736b02b0eba4a2488c0e649614f8 /server/sonar-web/src/main | |
parent | f062b56fd8656ee6239335a561474e25f73af5f5 (diff) | |
download | sonarqube-76c1f065c934a9abcba93dedb97fe7b4b8277ec4.tar.gz sonarqube-76c1f065c934a9abcba93dedb97fe7b4b8277ec4.zip |
SONAR-19709 Migrate projects facets to the new UI
Diffstat (limited to 'server/sonar-web/src/main')
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]; +} |