diff options
author | 7PH <benjamin.raymond@sonarsource.com> | 2023-05-15 15:48:18 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-05-24 20:03:13 +0000 |
commit | 28a8a0f6d2e079e16a511d4112dbf17e66c514f7 (patch) | |
tree | b3392894adb008235d1daf07419e97dbc4b3948f | |
parent | fd88c0ae2735b35e7ecd407105bbd0026bf6596e (diff) | |
download | sonarqube-28a8a0f6d2e079e16a511d4112dbf17e66c514f7.tar.gz sonarqube-28a8a0f6d2e079e16a511d4112dbf17e66c514f7.zip |
SONAR-19236 Implement new hotspot sidebar header
5 files changed, 254 insertions, 161 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx index b39c2c0b297..0b5d3eae408 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx @@ -24,7 +24,7 @@ import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security- import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions'; import withComponentContext from '../../app/components/componentContext/withComponentContext'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; -import { Location, withRouter } from '../../components/hoc/withRouter'; +import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { getLeakValue } from '../../components/measure/utils'; import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpers/branch-like'; import { isInput } from '../../helpers/keyboardEventHelpers'; @@ -55,6 +55,7 @@ interface OwnProps { currentUser: CurrentUser; component: Component; location: Location; + router: Router; } type Props = DispatchProps & OwnProps; @@ -410,6 +411,20 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { ); }; + handleShowAllHotspots = () => { + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + file: undefined, + fileUuid: undefined, + hotspots: [], + sinceLeakPeriod: undefined, + assignedToMe: undefined, + id: this.props.component.key, + }, + }); + }; + handleChangeStatusFilter = (status: HotspotStatusFilter) => { this.handleChangeFilters({ status }); }; @@ -519,6 +534,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { loadingMeasure={loadingMeasure} loadingMore={loadingMore} onChangeFilters={this.handleChangeFilters} + onShowAllHotspots={this.handleShowAllHotspots} onHotspotClick={this.handleHotspotClick} onLoadMore={this.handleLoadMore} onSwitchStatusFilter={this.handleChangeStatusFilter} @@ -526,6 +542,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { onLocationClick={this.handleLocationClick} securityCategories={standards[SecurityStandard.SONARSOURCE]} selectedHotspot={selectedHotspot} + selectedHotspotLocation={selectedHotspotLocationIndex} standards={standards} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx index 6ef57e100d6..f37e044ce2a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx @@ -62,6 +62,7 @@ export interface SecurityHotspotsAppRendererProps { loadingMeasure: boolean; loadingMore: boolean; onChangeFilters: (filters: Partial<HotspotFilters>) => void; + onShowAllHotspots: VoidFunction; onHotspotClick: (hotspot: RawHotspot) => void; onLocationClick: (index?: number) => void; onLoadMore: () => void; @@ -92,6 +93,8 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe selectedHotspot, selectedHotspotLocation, standards, + onChangeFilters, + onShowAllHotspots, } = props; return ( @@ -100,22 +103,60 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe <Helmet title={translate('hotspots.page')} /> <A11ySkipTarget anchor="security_hotspots_main" /> - <FilterBar - component={component} - filters={filters} - hotspotsReviewedMeasure={hotspotsReviewedMeasure} - isStaticListOfHotspots={isStaticListOfHotspots} - loadingMeasure={loadingMeasure} - onBranch={isBranch(branchLike)} - onChangeFilters={props.onChangeFilters} - /> <LargeCenteredLayout id={MetricKey.security_hotspots}> <PageContentFontWrapper> <div className="sw-grid sw-grid-cols-12 sw-w-full sw-body-sm"> <DeferredSpinner className="sw-mt-3" loading={loading} /> - {!loading && - (hotspots.length === 0 || !selectedHotspot ? ( + <StyledFilterbar className="sw-col-span-4 sw-rounded-t-1 sw-mt-0 sw-z-filterbar sw-p-4 it__hotspot-list"> + <FilterBar + component={component} + filters={filters} + hotspotsReviewedMeasure={hotspotsReviewedMeasure} + isStaticListOfHotspots={isStaticListOfHotspots} + loadingMeasure={loadingMeasure} + onBranch={isBranch(branchLike)} + onChangeFilters={onChangeFilters} + onShowAllHotspots={onShowAllHotspots} + /> + {hotspots.length > 0 && selectedHotspot && ( + <> + {filterByCategory || filterByCWE || filterByFile ? ( + <HotspotSimpleList + filterByCategory={filterByCategory} + filterByCWE={filterByCWE} + filterByFile={filterByFile} + hotspots={hotspots} + hotspotsTotal={hotspotsTotal} + loadingMore={loadingMore} + onHotspotClick={props.onHotspotClick} + onLoadMore={props.onLoadMore} + onLocationClick={props.onLocationClick} + selectedHotspotLocation={selectedHotspotLocation} + selectedHotspot={selectedHotspot} + standards={standards} + /> + ) : ( + <HotspotList + hotspots={hotspots} + hotspotsTotal={hotspotsTotal} + isStaticListOfHotspots={isStaticListOfHotspots} + loadingMore={loadingMore} + onHotspotClick={props.onHotspotClick} + onLoadMore={props.onLoadMore} + onLocationClick={props.onLocationClick} + securityCategories={securityCategories} + selectedHotspot={selectedHotspot} + selectedHotspotLocation={selectedHotspotLocation} + statusFilter={filters.status} + /> + )} + </> + )} + </StyledFilterbar> + + <main className="sw-col-span-8"> + {hotspots.length === 0 || !selectedHotspot ? ( <EmptyHotspotsPage filtered={ filters.assignedToMe || @@ -126,53 +167,17 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe isStaticListOfHotspots={isStaticListOfHotspots} /> ) : ( - <> - <FilterbarStyled className="sw-col-span-4 sw-rounded-t-1 sw-mt-0 sw-z-filterbar sw-p-4 it__hotspot-list"> - {filterByCategory || filterByCWE || filterByFile ? ( - <HotspotSimpleList - filterByCategory={filterByCategory} - filterByCWE={filterByCWE} - filterByFile={filterByFile} - hotspots={hotspots} - hotspotsTotal={hotspotsTotal} - loadingMore={loadingMore} - onHotspotClick={props.onHotspotClick} - onLoadMore={props.onLoadMore} - onLocationClick={props.onLocationClick} - selectedHotspotLocation={selectedHotspotLocation} - selectedHotspot={selectedHotspot} - standards={standards} - /> - ) : ( - <HotspotList - hotspots={hotspots} - hotspotsTotal={hotspotsTotal} - isStaticListOfHotspots={isStaticListOfHotspots} - loadingMore={loadingMore} - onHotspotClick={props.onHotspotClick} - onLoadMore={props.onLoadMore} - onLocationClick={props.onLocationClick} - securityCategories={securityCategories} - selectedHotspot={selectedHotspot} - selectedHotspotLocation={selectedHotspotLocation} - statusFilter={filters.status} - /> - )} - </FilterbarStyled> - - <main className="sw-col-span-8"> - <HotspotViewer - component={component} - hotspotKey={selectedHotspot.key} - hotspotsReviewedMeasure={hotspotsReviewedMeasure} - onSwitchStatusFilter={props.onSwitchStatusFilter} - onUpdateHotspot={props.onUpdateHotspot} - onLocationClick={props.onLocationClick} - selectedHotspotLocation={selectedHotspotLocation} - /> - </main> - </> - ))} + <HotspotViewer + component={component} + hotspotKey={selectedHotspot.key} + hotspotsReviewedMeasure={hotspotsReviewedMeasure} + onSwitchStatusFilter={props.onSwitchStatusFilter} + onUpdateHotspot={props.onUpdateHotspot} + onLocationClick={props.onLocationClick} + selectedHotspotLocation={selectedHotspotLocation} + /> + )} + </main> </div> </PageContentFontWrapper> </LargeCenteredLayout> @@ -180,9 +185,8 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe ); } -const FilterbarStyled = withTheme( +const StyledFilterbar = withTheme( styled.div` - position: sticky; box-sizing: border-box; overflow-x: hidden; overflow-y: auto; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx index 2f4accf4d20..7564ee0962f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx @@ -21,7 +21,6 @@ import { act, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Route } from 'react-router-dom'; -import selectEvent from 'react-select-event'; import { byDisplayValue, byRole, byTestId, byText } from 'testing-library-selector'; import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock'; import SecurityHotspotServiceMock from '../../../api/mocks/SecurityHotspotServiceMock'; @@ -52,26 +51,29 @@ const ui = { editAssigneeButton: byRole('button', { name: 'hotspots.assignee.change_user', }), - filterAssigneeToMe: byRole('button', { + filterAssigneeToMe: byRole('checkbox', { name: 'hotspot.filters.assignee.assigned_to_me', }), - filterSeeAll: byRole('button', { name: 'hotspot.filters.assignee.all' }), + clearFilters: byRole('menuitem', { name: 'hotspot.filters.clear' }), + filterDropdown: byRole('button', { name: 'hotspot.filters.title' }), + filterToReview: byRole('radio', { name: 'hotspot.filters.status.to_review' }), filterByStatus: byRole('combobox', { name: 'hotspot.filters.status' }), filterByPeriod: byRole('combobox', { name: 'hotspot.filters.period' }), + filterNewCode: byRole('checkbox', { name: 'hotspot.filters.period.since_leak_period' }), noHotspotForFilter: byText('hotspots.no_hotspots_for_filters.title'), selectStatus: byRole('button', { name: 'hotspots.status.select_status' }), toReviewStatus: byText('hotspots.status_option.TO_REVIEW'), changeStatus: byRole('button', { name: 'hotspots.status.change_status' }), hotspotTitle: (name: string | RegExp) => byRole('heading', { name }), hotspotStatus: byRole('heading', { name: 'status: hotspots.status_option.FIXED' }), - hotpostListTitle: byRole('heading', { name: 'hotspots.list_title.TO_REVIEW.4' }), + hotpostListTitle: byText('hotspots.list_title'), hotspotCommentBox: byRole('textbox', { name: 'hotspots.comment.field' }), commentSubmitButton: byRole('button', { name: 'hotspots.comment.submit' }), commentEditButton: byRole('button', { name: 'issue.comment.edit' }), commentDeleteButton: byRole('button', { name: 'issue.comment.delete' }), textboxWithText: (value: string) => byDisplayValue(value), activeAssignee: byTestId('assignee-name'), - successGlobalMessage: byRole('status'), + successGlobalMessage: byTestId('global-message__SUCCESS'), currentUserSelectionItem: byText('foo'), panel: byTestId('security-hotspot-test'), codeTab: byRole('tab', { name: 'hotspots.tabs.code' }), @@ -82,6 +84,7 @@ const ui = { vulnerabilityContent: byText('Assess'), fixTab: byRole('tab', { name: 'hotspots.tabs.fix_recommendations' }), fixContent: byText('This is how to fix'), + showAllHotspotLink: byRole('link', { name: 'hotspot.filters.show_all' }), }; const hotspotsHandler = new SecurityHotspotServiceMock(); @@ -99,6 +102,22 @@ describe('rendering', () => { ); expect(await screen.findAllByText('variant 1, variant 2')).toHaveLength(2); }); + + it('should render the simple list when a file is selected', async () => { + const user = userEvent.setup(); + renderSecurityHotspotsApp( + `security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&files=src%2Findex.js` + ); + + expect(ui.filterDropdown.query()).not.toBeInTheDocument(); + expect(ui.filterToReview.query()).not.toBeInTheDocument(); + + // Drop selection + await user.click(ui.showAllHotspotLink.get()); + + expect(ui.filterDropdown.get()).toBeInTheDocument(); + expect(ui.filterToReview.get()).toBeInTheDocument(); + }); }); it('should navigate when comming from SonarLint', async () => { @@ -292,9 +311,11 @@ it('should be able to filter the hotspot list', async () => { expect(await ui.hotpostListTitle.find()).toBeInTheDocument(); + await user.click(ui.filterDropdown.get()); await user.click(ui.filterAssigneeToMe.get()); expect(ui.noHotspotForFilter.get()).toBeInTheDocument(); - await selectEvent.select(ui.filterByStatus.get(), ['hotspot.filters.status.to_review']); + + await user.click(ui.filterToReview.get()); expect(getSecurityHotspots).toHaveBeenLastCalledWith({ inNewCodePeriod: false, @@ -306,7 +327,8 @@ it('should be able to filter the hotspot list', async () => { status: 'TO_REVIEW', }); - await selectEvent.select(ui.filterByPeriod.get(), ['hotspot.filters.period.since_leak_period']); + await user.click(ui.filterDropdown.get()); + await user.click(ui.filterNewCode.get()); expect(getSecurityHotspots).toHaveBeenLastCalledWith({ inNewCodePeriod: true, @@ -318,7 +340,8 @@ it('should be able to filter the hotspot list', async () => { status: 'TO_REVIEW', }); - await user.click(ui.filterSeeAll.get()); + await user.click(ui.filterDropdown.get()); + await user.click(ui.clearFilters.get()); expect(ui.hotpostListTitle.get()).toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx index 3be25c9785e..869e87986c6 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx @@ -17,18 +17,32 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { withTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { + CoverageIndicator, + DiscreetInteractiveIcon, + DiscreetLink, + Dropdown, + FilterIcon, + HelperHintIcon, + ItemCheckbox, + ItemDangerButton, + ItemDivider, + ItemHeader, + PopupPlacement, + ToggleButton, + themeBorder, +} from 'design-system'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; -import Link from '../../../components/common/Link'; -import ButtonToggle from '../../../components/controls/ButtonToggle'; import HelpTooltip from '../../../components/controls/HelpTooltip'; -import Select from '../../../components/controls/Select'; import Measure from '../../../components/measure/Measure'; -import CoverageRating from '../../../components/ui/CoverageRating'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; -import { getProjectSecurityHotspots } from '../../../helpers/urls'; import { ComponentQualifier } from '../../../types/component'; +import { MetricType } from '../../../types/metrics'; import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots'; import { Component } from '../../../types/types'; import { CurrentUser, isLoggedIn } from '../../../types/users'; @@ -42,6 +56,7 @@ export interface FilterBarProps { loadingMeasure: boolean; onBranch: boolean; onChangeFilters: (filters: Partial<HotspotFilters>) => void; + onShowAllHotspots: VoidFunction; } const statusOptions: Array<{ label: string; value: HotspotStatusFilter }> = [ @@ -54,116 +69,148 @@ const statusOptions: Array<{ label: string; value: HotspotStatusFilter }> = [ { value: HotspotStatusFilter.SAFE, label: translate('hotspot.filters.status.safe') }, ]; -const periodOptions = [ - { value: true, label: translate('hotspot.filters.period.since_leak_period') }, - { value: false, label: translate('hotspot.filters.period.overall') }, -]; - export enum AssigneeFilterOption { ALL = 'all', ME = 'me', } -const assigneeFilterOptions = [ - { value: AssigneeFilterOption.ME, label: translate('hotspot.filters.assignee.assigned_to_me') }, - { value: AssigneeFilterOption.ALL, label: translate('hotspot.filters.assignee.all') }, -]; - export function FilterBar(props: FilterBarProps) { const { currentUser, component, filters, hotspotsReviewedMeasure, - isStaticListOfHotspots, loadingMeasure, onBranch, + isStaticListOfHotspots, } = props; const isProject = component.qualifier === ComponentQualifier.Project; + const userLoggedIn = isLoggedIn(currentUser); + const filtersCount = Number(filters.assignedToMe) + Number(filters.inNewCodePeriod); + const isFiltered = Boolean(filtersCount); return ( - <div className="filter-bar-outer"> - <div className="filter-bar"> - <div className="filter-bar-inner display-flex-center"> - {isStaticListOfHotspots ? ( - <Link to={getProjectSecurityHotspots(component.key)}> - {translate('hotspot.filters.show_all')} - </Link> - ) : ( - <div className="display-flex-space-between width-100"> - <div className="display-flex-center"> - <h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3> - - {isLoggedIn(currentUser) && ( - <span className="huge-spacer-right"> - <ButtonToggle - onCheck={(value: AssigneeFilterOption) => - props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME }) - } - options={assigneeFilterOptions} - value={ - filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL - } - /> - </span> + <div className="sw-flex sw-flex-col sw-justify-between sw-pb-4 sw-mb-3"> + {isStaticListOfHotspots ? ( + <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4"> + <FormattedMessage + id="hotspot.filters.by_file_or_list_x" + values={{ + show_all_link: ( + <DiscreetLink + className="sw-ml-1" + onClick={props.onShowAllHotspots} + preventDefault={true} + to={{}} + > + {translate('hotspot.filters.show_all')} + </DiscreetLink> + ), + }} + defaultMessage={translate('hotspot.filters.by_file_or_list_x')} + /> + </StyledFilterWrapper> + ) : ( + <> + {isProject && ( + <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4 sw-items-center sw-h-6"> + <DeferredSpinner loading={loadingMeasure}> + {hotspotsReviewedMeasure !== undefined && ( + <CoverageIndicator value={hotspotsReviewedMeasure} /> )} - - <span className="spacer-right"> {translate('status')} </span> - <Select - className="input-medium big-spacer-right" - aria-label={translate('hotspot.filters.status')} - onChange={(option: { value: HotspotStatusFilter }) => - props.onChangeFilters({ status: option.value }) + <Measure + className="sw-ml-2 sw-body-sm-highlight" + metricKey={ + onBranch && !filters.inNewCodePeriod + ? 'security_hotspots_reviewed' + : 'new_security_hotspots_reviewed' } - options={statusOptions} - isSearchable={false} - value={statusOptions.find((status) => status.value === filters.status)} + metricType={MetricType.Percent} + value={hotspotsReviewedMeasure} /> + <span className="sw-ml-1 sw-body-sm"> + {translate('metric.security_hotspots_reviewed.name')} + </span> + <HelpTooltip className="sw-ml-1" overlay={translate('hotspots.reviewed.tooltip')}> + <HelperHintIcon aria-label="help-tooltip" /> + </HelpTooltip> + </DeferredSpinner> + </StyledFilterWrapper> + )} - {onBranch && ( - <Select - className="input-medium big-spacer-right" - aria-label={translate('hotspot.filters.period')} - onChange={(option: { value: boolean }) => - props.onChangeFilters({ inNewCodePeriod: option.value }) - } - options={periodOptions} - isSearchable={false} - value={periodOptions.find((period) => period.value === filters.inNewCodePeriod)} - /> - )} - </div> + <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4 sw-gap-2 sw-justify-between"> + <ToggleButton + aria-label={translate('hotspot.filters.status')} + onChange={(status: HotspotStatusFilter) => props.onChangeFilters({ status })} + options={statusOptions} + value={statusOptions.find((status) => status.value === filters.status)?.value} + /> + {(onBranch || userLoggedIn || isFiltered) && ( + <Dropdown + allowResizing={true} + closeOnClick={false} + id="filter-hotspots-menu" + overlay={ + <> + <ItemHeader>{translate('hotspot.filters.title')}</ItemHeader> - {isProject && ( - <div className="display-flex-center"> - <span className="little-spacer-right"> - {translate('metric.security_hotspots_reviewed.name')} - </span> - <HelpTooltip - className="big-spacer-right" - overlay={translate('hotspots.reviewed.tooltip')} - /> - <DeferredSpinner loading={loadingMeasure}> - {hotspotsReviewedMeasure && <CoverageRating value={hotspotsReviewedMeasure} />} - <Measure - className="spacer-left huge it__hs-review-percentage" - metricKey={ - onBranch && !filters.inNewCodePeriod - ? 'security_hotspots_reviewed' - : 'new_security_hotspots_reviewed' - } - metricType="PERCENT" - value={hotspotsReviewedMeasure} - /> - </DeferredSpinner> - </div> - )} - </div> - )} - </div> - </div> + {onBranch && ( + <ItemCheckbox + checked={Boolean(filters.inNewCodePeriod)} + onCheck={(inNewCodePeriod) => props.onChangeFilters({ inNewCodePeriod })} + > + <span className="sw-mx-2"> + {translate('hotspot.filters.period.since_leak_period')} + </span> + </ItemCheckbox> + )} + + {userLoggedIn && ( + <ItemCheckbox + checked={Boolean(filters.assignedToMe)} + onCheck={(assignedToMe) => props.onChangeFilters({ assignedToMe })} + > + <span className="sw-mx-2"> + {translate('hotspot.filters.assignee.assigned_to_me')} + </span> + </ItemCheckbox> + )} + + {isFiltered && <ItemDivider />} + + {isFiltered && ( + <ItemDangerButton + onClick={() => + props.onChangeFilters({ + assignedToMe: false, + inNewCodePeriod: false, + }) + } + > + {translate('hotspot.filters.clear')} + </ItemDangerButton> + )} + </> + } + placement={PopupPlacement.BottomRight} + > + <DiscreetInteractiveIcon + Icon={FilterIcon} + aria-label={translate('hotspot.filters.title')} + > + {isFiltered ? filtersCount : null} + </DiscreetInteractiveIcon> + </Dropdown> + )} + </StyledFilterWrapper> + </> + )} </div> ); } +const StyledFilterWrapper = withTheme(styled.div` + border-bottom: ${themeBorder('default')}; +`); + export default withCurrentUserContext(FilterBar); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index c55e4399937..10a0139e4a0 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -833,6 +833,7 @@ hotspots.continue_to_next_hotspot=Continue Reviewing hotspot.filters.title=Filters hotspot.filters.assignee.assigned_to_me=Assigned to me hotspot.filters.assignee.all=All +hotspot.filters.clear=Clear filters hotspot.filters.status=Status filter hotspot.filters.status.to_review=To review hotspot.filters.status.acknowledged=Acknowledged @@ -841,7 +842,8 @@ hotspot.filters.period=Period filter hotspot.filters.period.since_leak_period=New Code hotspot.filters.period.overall=Overall code hotspot.filters.status.safe=Safe -hotspot.filters.show_all=Show all hotspots +hotspot.filters.by_file_or_list_x=Your hotspots are currently filtered, {show_all_link} +hotspot.filters.show_all=show all hotspots hotspot.section.activity=Recent activity: hotspots.reviewed.tooltip=Percentage of open Security Hotspots that have been reviewed (Acknowledged, Fixed or Safe) |