]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19236 Implement new hotspot sidebar header
author7PH <benjamin.raymond@sonarsource.com>
Mon, 15 May 2023 13:48:18 +0000 (15:48 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 24 May 2023 20:03:13 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b39c2c0b297d058bdf5bdf64dd6ef0cd8839aaad..0b5d3eae408f1c55f305eb53b4639393ecd7e922 100644 (file)
@@ -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}
       />
     );
index 6ef57e100d6b90a0cad1f43d1cf6eabf2d1bcce1..f37e044ce2ab67658f8f1117932f008326f7690b 100644 (file)
@@ -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;
index 2f4accf4d2079c874d3f82f753a432cb7a741481..7564ee0962fa14c84b6d44c80bfd87bd2d3bb24d 100644 (file)
@@ -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();
 });
index 3be25c9785e03e4654d501b01c21ed5168d0ea7a..869e87986c65e5bf3c9c0a2de5147e7440298c9b 100644 (file)
  * 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);
index c55e43999370f233c0c37f9ad405a0ddcffbe6cf..10a0139e4a0bf53ea200baed183a08cd56f95db2 100644 (file)
@@ -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)