]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12797 Security Hotspots page allows to filter by hotspots keys param
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Mon, 23 Dec 2019 13:31:54 +0000 (14:31 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:33 +0000 (20:46 +0100)
14 files changed:
server/sonar-web/src/main/js/api/security-hotspots.ts
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/FilterBar-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 78ec7567f62385a1e2232bc73928acdda409ee1f..ced115b7e49131c995840aaa663a235b0eb8090b 100644 (file)
@@ -58,6 +58,10 @@ export function getSecurityHotspots(
   return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
 }
 
+export function getSecurityHotspotList(hotspotKeys: string[]): Promise<HotspotSearchResponse> {
+  return getJSON('/api/hotspots/search', { hotspots: hotspotKeys.join() }).catch(throwGlobalError);
+}
+
 export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<Hotspot> {
   return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey })
     .then((response: Hotspot & { users: T.UserBase[] }) => {
index a1170faef13194dbf9f23e28d97fb5c12eaa7d15..4856ce425afb4c16f2816d2a5d5d82973b2a8f17 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 { Location } from 'history';
 import * as React from 'react';
 import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
-import { getSecurityHotspots } from '../../api/security-hotspots';
+import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots';
 import { withCurrentUser } from '../../components/hoc/withCurrentUser';
+import { Router } from '../../components/hoc/withRouter';
 import { getBranchLikeQuery } from '../../helpers/branch-like';
 import { getStandards } from '../../helpers/security-standard';
 import { isLoggedIn } from '../../helpers/users';
@@ -43,9 +45,12 @@ interface Props {
   branchLike?: BranchLike;
   currentUser: T.CurrentUser;
   component: T.Component;
+  location: Location;
+  router: Router;
 }
 
 interface State {
+  hotspotKeys?: string[];
   hotspots: RawHotspot[];
   loading: boolean;
   securityCategories: T.StandardSecurityCategories;
@@ -79,7 +84,10 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
   }
 
   componentDidUpdate(previous: Props) {
-    if (this.props.component.key !== previous.component.key) {
+    if (
+      this.props.component.key !== previous.component.key ||
+      this.props.location.query.hotspots !== previous.location.query.hotspots
+    ) {
       this.fetchInitialData();
     }
   }
@@ -115,9 +123,19 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
   }
 
   fetchSecurityHotspots() {
-    const { branchLike, component } = this.props;
+    const { branchLike, component, location } = this.props;
     const { filters } = this.state;
 
+    const hotspotKeys = location.query.hotspots
+      ? (location.query.hotspots as string).split(',')
+      : undefined;
+
+    this.setState({ hotspotKeys });
+
+    if (hotspotKeys && hotspotKeys.length > 0) {
+      return getSecurityHotspotList(hotspotKeys);
+    }
+
     const status =
       filters.status === HotspotStatusFilter.TO_REVIEW
         ? HotspotStatus.TO_REVIEW
@@ -187,18 +205,34 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
     });
   };
 
+  handleShowAllHotspots = () => {
+    this.props.router.push({
+      ...this.props.location,
+      query: { ...this.props.location.query, hotspots: undefined }
+    });
+  };
+
   render() {
     const { branchLike } = this.props;
-    const { hotspots, loading, securityCategories, selectedHotspotKey, filters } = this.state;
+    const {
+      hotspotKeys,
+      hotspots,
+      loading,
+      securityCategories,
+      selectedHotspotKey,
+      filters
+    } = this.state;
 
     return (
       <SecurityHotspotsAppRenderer
         branchLike={branchLike}
         filters={filters}
         hotspots={hotspots}
+        isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)}
         loading={loading}
         onChangeFilters={this.handleChangeFilters}
         onHotspotClick={this.handleHotspotClick}
+        onShowAllHotspots={this.handleShowAllHotspots}
         onUpdateHotspot={this.handleHotspotUpdate}
         securityCategories={securityCategories}
         selectedHotspotKey={selectedHotspotKey}
index 0ffe2fa8b5f3b0b7e1c2a3438fb79971d70787f0..593e13b92914c47e578c68a8e49f575f130c96a9 100644 (file)
@@ -37,20 +37,35 @@ export interface SecurityHotspotsAppRendererProps {
   branchLike?: BranchLike;
   filters: HotspotFilters;
   hotspots: RawHotspot[];
+  isStaticListOfHotspots: boolean;
   loading: boolean;
   onChangeFilters: (filters: Partial<HotspotFilters>) => void;
   onHotspotClick: (key: string) => void;
+  onShowAllHotspots: () => void;
   onUpdateHotspot: (hotspot: HotspotUpdate) => void;
   selectedHotspotKey?: string;
   securityCategories: T.StandardSecurityCategories;
 }
 
 export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
-  const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey, filters } = props;
+  const {
+    branchLike,
+    hotspots,
+    isStaticListOfHotspots,
+    loading,
+    securityCategories,
+    selectedHotspotKey,
+    filters
+  } = props;
 
   return (
     <div id="security_hotspots">
-      <FilterBar onChangeFilters={props.onChangeFilters} filters={filters} />
+      <FilterBar
+        filters={filters}
+        isStaticListOfHotspots={isStaticListOfHotspots}
+        onChangeFilters={props.onChangeFilters}
+        onShowAllHotspots={props.onShowAllHotspots}
+      />
       <ScreenPositionHelper>
         {({ top }) => (
           <div className="wrapper" style={{ top }}>
@@ -84,6 +99,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
                   <div className="sidebar">
                     <HotspotList
                       hotspots={hotspots}
+                      isStaticListOfHotspots={isStaticListOfHotspots}
                       onHotspotClick={props.onHotspotClick}
                       securityCategories={securityCategories}
                       selectedHotspotKey={selectedHotspotKey}
index d4623c843a2191d6f66ceff5e9d3f93bb586fae8..b50b0790cbc57715b29e5eee053244ae906d0c69 100644 (file)
@@ -21,11 +21,16 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { addNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { getSecurityHotspots } from '../../../api/security-hotspots';
+import { getSecurityHotspotList, getSecurityHotspots } from '../../../api/security-hotspots';
 import { mockBranch } from '../../../helpers/mocks/branch-like';
 import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
 import { getStandards } from '../../../helpers/security-standard';
-import { mockComponent, mockCurrentUser } from '../../../helpers/testMocks';
+import {
+  mockComponent,
+  mockCurrentUser,
+  mockLocation,
+  mockRouter
+} from '../../../helpers/testMocks';
 import {
   HotspotResolution,
   HotspotStatus,
@@ -34,13 +39,16 @@ import {
 import { SecurityHotspotsApp } from '../SecurityHotspotsApp';
 import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
 
+beforeEach(() => jest.clearAllMocks());
+
 jest.mock('sonar-ui-common/helpers/pages', () => ({
   addNoFooterPageClass: jest.fn(),
   removeNoFooterPageClass: jest.fn()
 }));
 
 jest.mock('../../../api/security-hotspots', () => ({
-  getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
+  getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] }),
+  getSecurityHotspotList: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
 }));
 
 jest.mock('../../../helpers/security-standard', () => ({
@@ -84,6 +92,54 @@ it('should load data correctly', async () => {
   expect(wrapper.state());
 });
 
+it('should load data correctly when hotspot key list is forced', async () => {
+  const sonarsourceSecurity = { cat1: { title: 'cat 1' } };
+  (getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity });
+
+  const hotspots = [
+    mockRawHotspot({ key: 'test1' }),
+    mockRawHotspot({ key: 'test2' }),
+    mockRawHotspot({ key: 'test3' })
+  ];
+  const hotspotKeys = hotspots.map(h => h.key);
+  (getSecurityHotspotList as jest.Mock).mockResolvedValueOnce({
+    hotspots
+  });
+
+  const location = mockLocation({ query: { hotspots: hotspotKeys.join() } });
+  const router = mockRouter();
+  const wrapper = shallowRender({
+    location,
+    router
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(getSecurityHotspotList).toBeCalledWith(hotspotKeys);
+  expect(wrapper.state().hotspotKeys).toEqual(hotspotKeys);
+  expect(wrapper.find(SecurityHotspotsAppRenderer).props().isStaticListOfHotspots).toBeTruthy();
+
+  // Reset
+  (getSecurityHotspots as jest.Mock).mockClear();
+  (getSecurityHotspotList as jest.Mock).mockClear();
+  wrapper
+    .find(SecurityHotspotsAppRenderer)
+    .props()
+    .onShowAllHotspots();
+  expect(router.push).toHaveBeenCalledWith({
+    ...location,
+    query: { ...location.query, hotspots: undefined }
+  });
+
+  // Simulate a new location
+  wrapper.setProps({
+    location: { ...location, query: { ...location.query, hotspots: undefined } }
+  });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().hotspotKeys).toBeUndefined();
+  expect(getSecurityHotspotList).not.toHaveBeenCalled();
+  expect(getSecurityHotspots).toHaveBeenCalled();
+});
+
 it('should handle hotspot update', async () => {
   const key = 'hotspotKey';
   const hotspots = [mockRawHotspot(), mockRawHotspot({ key })];
@@ -153,6 +209,8 @@ function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) {
       branchLike={branch}
       component={mockComponent()}
       currentUser={mockCurrentUser()}
+      location={mockLocation()}
+      router={mockRouter()}
       {...props}
     />
   );
index 9676eaa8715fb3bdf68e41dace8598f025011aab..a721ed06963159f14668c4be853b15626476bc37 100644 (file)
@@ -22,6 +22,7 @@ import * as React from 'react';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
 import { HotspotStatusFilter } from '../../../types/security-hotspots';
+import FilterBar from '../components/FilterBar';
 import SecurityHotspotsAppRenderer, {
   SecurityHotspotsAppRendererProps
 } from '../SecurityHotspotsAppRenderer';
@@ -49,13 +50,27 @@ it('should render correctly with hotspots', () => {
   ).toMatchSnapshot();
 });
 
+it('should properly propagate the "show all" call', () => {
+  const onShowAllHotspots = jest.fn();
+  const wrapper = shallowRender({ onShowAllHotspots });
+
+  wrapper
+    .find(FilterBar)
+    .props()
+    .onShowAllHotspots();
+
+  expect(onShowAllHotspots).toHaveBeenCalled();
+});
+
 function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
   return shallow(
     <SecurityHotspotsAppRenderer
       hotspots={[]}
+      isStaticListOfHotspots={true}
       loading={false}
       onChangeFilters={jest.fn()}
       onHotspotClick={jest.fn()}
+      onShowAllHotspots={jest.fn()}
       onUpdateHotspot={jest.fn()}
       securityCategories={{}}
       filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }}
index 11eeee50a36c8995cb6b4942555bbfe86bfaaea6..f76e3f7b88cba9945e78548400d244f25d158857 100644 (file)
@@ -17,9 +17,11 @@ exports[`should render correctly 1`] = `
     }
   }
   hotspots={Array []}
+  isStaticListOfHotspots={false}
   loading={true}
   onChangeFilters={[Function]}
   onHotspotClick={[Function]}
+  onShowAllHotspots={[Function]}
   onUpdateHotspot={[Function]}
   securityCategories={Object {}}
 />
index 9994b676832433db804ded2b63b9e3d1bd6ea629..cb0a3655f9c3fa5ca946fc36421d07029f959586 100644 (file)
@@ -11,7 +11,9 @@ exports[`should render correctly 1`] = `
         "status": "TO_REVIEW",
       }
     }
+    isStaticListOfHotspots={true}
     onChangeFilters={[MockFunction]}
+    onShowAllHotspots={[MockFunction]}
   />
   <ScreenPositionHelper>
     <Component />
@@ -150,6 +152,7 @@ exports[`should render correctly with hotspots 1`] = `
                 },
               ]
             }
+            isStaticListOfHotspots={true}
             onHotspotClick={[MockFunction]}
             securityCategories={Object {}}
             statusFilter="TO_REVIEW"
@@ -231,6 +234,7 @@ exports[`should render correctly with hotspots 2`] = `
                 },
               ]
             }
+            isStaticListOfHotspots={true}
             onHotspotClick={[MockFunction]}
             securityCategories={Object {}}
             selectedHotspotKey="h2"
index 3d1050e40f8cdcae249e22bf940e18471f4803dc..4f42abac06f434f4c518fbd7334adabbb1a75a1a 100644 (file)
@@ -28,7 +28,9 @@ import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hot
 export interface FilterBarProps {
   currentUser: T.CurrentUser;
   filters: HotspotFilters;
+  isStaticListOfHotspots: boolean;
   onChangeFilters: (filters: Partial<HotspotFilters>) => void;
+  onShowAllHotspots: () => void;
 }
 
 const statusOptions: Array<{ label: string; value: string }> = [
@@ -48,34 +50,43 @@ const assigneeFilterOptions = [
 ];
 
 export function FilterBar(props: FilterBarProps) {
-  const { currentUser, filters } = props;
+  const { currentUser, filters, isStaticListOfHotspots } = props;
+
   return (
     <div className="filter-bar display-flex-center">
-      <h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3>
+      {isStaticListOfHotspots ? (
+        <a id="show_all_hotspot" onClick={() => props.onShowAllHotspots()} role="link" tabIndex={0}>
+          {translate('hotspot.filters.show_all')}
+        </a>
+      ) : (
+        <>
+          <h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3>
 
-      {isLoggedIn(currentUser) && (
-        <RadioToggle
-          className="huge-spacer-right"
-          name="assignee-filter"
-          onCheck={(value: AssigneeFilterOption) =>
-            props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME })
-          }
-          options={assigneeFilterOptions}
-          value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL}
-        />
-      )}
+          {isLoggedIn(currentUser) && (
+            <RadioToggle
+              className="huge-spacer-right"
+              name="assignee-filter"
+              onCheck={(value: AssigneeFilterOption) =>
+                props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME })
+              }
+              options={assigneeFilterOptions}
+              value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL}
+            />
+          )}
 
-      <span className="spacer-right">{translate('status')}</span>
-      <Select
-        className="input-medium big-spacer-right"
-        clearable={false}
-        onChange={(option: { value: HotspotStatusFilter }) =>
-          props.onChangeFilters({ status: option.value })
-        }
-        options={statusOptions}
-        searchable={false}
-        value={filters.status}
-      />
+          <span className="spacer-right">{translate('status')}</span>
+          <Select
+            className="input-medium big-spacer-right"
+            clearable={false}
+            onChange={(option: { value: HotspotStatusFilter }) =>
+              props.onChangeFilters({ status: option.value })
+            }
+            options={statusOptions}
+            searchable={false}
+            value={filters.status}
+          />
+        </>
+      )}
     </div>
   );
 }
index 7b706d4ea2ee97da0dd853eb6a0631a7f8af3b25..990cfebfa02c4c27dacd017d46bea8b9e632b759 100644 (file)
@@ -29,6 +29,7 @@ import './HotspotList.css';
 
 export interface HotspotListProps {
   hotspots: RawHotspot[];
+  isStaticListOfHotspots: boolean;
   onHotspotClick: (key: string) => void;
   securityCategories: T.StandardSecurityCategories;
   selectedHotspotKey: string | undefined;
@@ -36,7 +37,13 @@ export interface HotspotListProps {
 }
 
 export default function HotspotList(props: HotspotListProps) {
-  const { hotspots, securityCategories, selectedHotspotKey, statusFilter } = props;
+  const {
+    hotspots,
+    isStaticListOfHotspots,
+    securityCategories,
+    selectedHotspotKey,
+    statusFilter
+  } = props;
 
   const groupedHotspots: Array<{
     risk: RiskExposure;
@@ -54,7 +61,10 @@ export default function HotspotList(props: HotspotListProps) {
     <>
       <h1 className="hotspot-list-header bordered-bottom">
         <SecurityHotspotIcon className="spacer-right" />
-        {translateWithParameters(`hotspots.list_title.${statusFilter}`, hotspots.length)}
+        {translateWithParameters(
+          isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`,
+          hotspots.length
+        )}
       </h1>
       <ul className="huge-spacer-bottom">
         {groupedHotspots.map(riskGroup => (
index ec47ceec6244f76a4a5a061f76bba60b76a5d25d..ff9536372b2f50c8efcda91d7b69deefb36d6c19 100644 (file)
@@ -30,6 +30,19 @@ it('should render correctly', () => {
   expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged-in');
 });
 
+it('should render correctly when the list of hotspot is static', () => {
+  const onShowAllHotspots = jest.fn();
+
+  const wrapper = shallowRender({
+    isStaticListOfHotspots: true,
+    onShowAllHotspots
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('a').simulate('click');
+  expect(onShowAllHotspots).toHaveBeenCalled();
+});
+
 it('should trigger onChange for status', () => {
   const onChangeFilters = jest.fn();
   const wrapper = shallowRender({ onChangeFilters });
@@ -60,7 +73,9 @@ function shallowRender(props: Partial<FilterBarProps> = {}) {
   return shallow(
     <FilterBar
       currentUser={mockCurrentUser()}
+      isStaticListOfHotspots={false}
       onChangeFilters={jest.fn()}
+      onShowAllHotspots={jest.fn()}
       filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }}
       {...props}
     />
index e01aae707bf8853a3dec9abc8bf84839d41e4411..b1d597886207d3ac601efa30ee1b05a982e2aebb 100644 (file)
@@ -27,6 +27,10 @@ it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
 });
 
+it('should render correctly when the list of hotspot is static', () => {
+  expect(shallowRender({ isStaticListOfHotspots: true })).toMatchSnapshot();
+});
+
 it('should render correctly with hotspots', () => {
   const hotspots = [
     mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
@@ -54,6 +58,7 @@ function shallowRender(props: Partial<HotspotListProps> = {}) {
   return shallow(
     <HotspotList
       hotspots={[]}
+      isStaticListOfHotspots={false}
       onHotspotClick={jest.fn()}
       securityCategories={{}}
       selectedHotspotKey="h2"
index 938aa248d73a06f2dae63f1a476b8f525190079f..f5c80bb3d3e6077e48235278860d31bf3a3a8e7b 100644 (file)
@@ -1,5 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`should render correctly when the list of hotspot is static 1`] = `
+<div
+  className="filter-bar display-flex-center"
+>
+  <a
+    id="show_all_hotspot"
+    onClick={[Function]}
+    role="link"
+    tabIndex={0}
+  >
+    hotspot.filters.show_all
+  </a>
+</div>
+`;
+
 exports[`should render correctly: anonymous 1`] = `
 <div
   className="filter-bar display-flex-center"
index f222cf6e39e328962b5f4786077f8f526e803f8d..4deb2cea0a17867122c359cdadaa5a8a62f895ef 100644 (file)
@@ -16,6 +16,22 @@ exports[`should render correctly 1`] = `
 </Fragment>
 `;
 
+exports[`should render correctly when the list of hotspot is static 1`] = `
+<Fragment>
+  <h1
+    className="hotspot-list-header bordered-bottom"
+  >
+    <SecurityHotspotIcon
+      className="spacer-right"
+    />
+    hotspots.list_title.0
+  </h1>
+  <ul
+    className="huge-spacer-bottom"
+  />
+</Fragment>
+`;
+
 exports[`should render correctly with hotspots 1`] = `
 <Fragment>
   <h1
index 4a6b30d3984afbca327ad290505ff7107caab4ef..3c1dc389d2283d92e3e94044e93e0d1d8a4f8438 100644 (file)
@@ -648,6 +648,7 @@ hotspots.page=Security Hotspots
 hotspots.no_hotspots.title=There are no Security Hotspots to review
 hotspots.no_hotspots.description=Next time you analyse a piece of code that contains a potential security risk, it will show up here.
 hotspots.learn_more=Learn more about Security Hotspots
+hotspots.list_title={0} Security Hotspots
 hotspots.list_title.TO_REVIEW={0} Security Hotspots to review
 hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed
 hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe
@@ -676,6 +677,7 @@ hotspot.filters.assignee.all=All
 hotspot.filters.status.to_review=To review
 hotspot.filters.status.fixed=Reviewed as fixed
 hotspot.filters.status.safe=Reviewed as safe
+hotspot.filters.show_all=Show all hotspots
 hotspots.review_hotspot=Review Hotspot
 
 hotspots.form.title=Mark Security Hotspot as: