Browse Source

SONAR-12719 Improve visual feedback of hotspots status update

tags/8.2.0.32929
Jeremy Davis 4 years ago
parent
commit
264d88bf2e
22 changed files with 549 additions and 342 deletions
  1. 30
    24
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  2. 13
    20
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
  3. 29
    21
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
  4. 2
    1
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  5. 21
    55
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
  6. 1
    10
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
  7. 12
    9
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
  8. 2
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
  9. 104
    58
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
  10. 2
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
  11. 3
    7
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
  12. 1
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  13. 26
    7
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx
  14. 44
    23
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx
  15. 1
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
  16. 59
    62
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
  17. 168
    32
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap
  18. 7
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx
  19. 6
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx
  20. 10
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
  21. 5
    8
      server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
  22. 3
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 30
- 24
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Location } from 'history';
import { flatMap, range } from 'lodash';
import * as React from 'react';
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { getMeasures } from '../../api/measures';
@@ -35,7 +36,6 @@ import {
HotspotResolution,
HotspotStatus,
HotspotStatusFilter,
HotspotUpdate,
RawHotspot
} from '../../types/security-hotspots';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
@@ -61,7 +61,7 @@ interface State {
loadingMeasure: boolean;
loadingMore: boolean;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
selectedHotspot: RawHotspot | undefined;
filters: HotspotFilters;
}

@@ -79,7 +79,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
hotspots: [],
hotspotsPageIndex: 1,
securityCategories: {},
selectedHotspotKey: undefined,
selectedHotspot: undefined,
filters: {
...this.constructFiltersFromProps(props),
status: HotspotStatusFilter.TO_REVIEW
@@ -150,13 +150,13 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
hotspotsTotal: paging.total,
loading: false,
securityCategories: sonarsourceSecurity,
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined
selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined
});
})
.catch(this.handleCallFailure);
}

fetchSecurityHotspotsReviewed() {
fetchSecurityHotspotsReviewed = () => {
const { branchLike, component } = this.props;
const { filters } = this.state;

@@ -186,7 +186,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
this.setState({ loadingMeasure: false });
}
});
}
};

fetchSecurityHotspots(page = 1) {
const { branchLike, component, location } = this.props;
@@ -241,7 +241,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
hotspotsPageIndex: 1,
hotspotsTotal: paging.total,
loading: false,
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined
selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined
});
})
.catch(this.handleCallFailure);
@@ -259,24 +259,30 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
);
};

handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key });
handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot });

handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => {
this.setState(({ hotspots }) => {
const index = hotspots.findIndex(h => h.key === key);
handleHotspotUpdate = (hotspotKey: string) => {
const { hotspots, hotspotsPageIndex } = this.state;
const index = hotspots.findIndex(h => h.key === hotspotKey);

if (index > -1) {
const hotspot = {
...hotspots[index],
status,
resolution
};
return Promise.all(
range(hotspotsPageIndex).map(p => this.fetchSecurityHotspots(p + 1 /* pages are 1-indexed */))
)
.then(hotspotPages => {
const allHotspots = flatMap(hotspotPages, 'hotspots');

return { hotspots: [...hotspots.slice(0, index), hotspot, ...hotspots.slice(index + 1)] };
}
return null;
});
return this.fetchSecurityHotspotsReviewed();
const { paging } = hotspotPages[hotspotPages.length - 1];

const nextHotspot = allHotspots[Math.min(index, allHotspots.length - 1)];

this.setState({
hotspots: allHotspots,
hotspotsPageIndex: paging.pageIndex,
hotspotsTotal: paging.total,
selectedHotspot: nextHotspot
});
})
.then(this.fetchSecurityHotspotsReviewed);
};

handleShowAllHotspots = () => {
@@ -317,7 +323,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
loadingMeasure,
loadingMore,
securityCategories,
selectedHotspotKey,
selectedHotspot,
filters
} = this.state;

@@ -339,7 +345,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
onShowAllHotspots={this.handleShowAllHotspots}
onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
selectedHotspot={selectedHotspot}
/>
);
}

+ 13
- 20
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx View File

@@ -26,12 +26,7 @@ import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
import { isBranch } from '../../helpers/branch-like';
import { BranchLike } from '../../types/branch-like';
import {
HotspotFilters,
HotspotStatusFilter,
HotspotUpdate,
RawHotspot
} from '../../types/security-hotspots';
import { HotspotFilters, HotspotStatusFilter, RawHotspot } from '../../types/security-hotspots';
import EmptyHotspotsPage from './components/EmptyHotspotsPage';
import FilterBar from './components/FilterBar';
import HotspotList from './components/HotspotList';
@@ -50,11 +45,11 @@ export interface SecurityHotspotsAppRendererProps {
loadingMeasure: boolean;
loadingMore: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
onHotspotClick: (key: string) => void;
onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
onShowAllHotspots: () => void;
onUpdateHotspot: (hotspot: HotspotUpdate) => void;
selectedHotspotKey?: string;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
selectedHotspot: RawHotspot | undefined;
securityCategories: T.StandardSecurityCategories;
}

@@ -70,7 +65,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
loadingMeasure,
loadingMore,
securityCategories,
selectedHotspotKey,
selectedHotspot,
filters
} = props;

@@ -98,7 +93,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
<DeferredSpinner className="huge-spacer-left big-spacer-top" />
) : (
<>
{hotspots.length === 0 ? (
{hotspots.length === 0 || !selectedHotspot ? (
<EmptyHotspotsPage
filtered={
filters.assignedToMe ||
@@ -118,19 +113,17 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
onHotspotClick={props.onHotspotClick}
onLoadMore={props.onLoadMore}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
selectedHotspot={selectedHotspot}
statusFilter={filters.status}
/>
</div>
<div className="main">
{selectedHotspotKey && (
<HotspotViewer
branchLike={branchLike}
hotspotKey={selectedHotspotKey}
onUpdateHotspot={props.onUpdateHotspot}
securityCategories={securityCategories}
/>
)}
<HotspotViewer
branchLike={branchLike}
hotspotKey={selectedHotspot.key}
onUpdateHotspot={props.onUpdateHotspot}
securityCategories={securityCategories}
/>
</div>
</div>
)}

+ 29
- 21
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx View File

@@ -99,7 +99,7 @@ it('should load data correctly', async () => {

expect(wrapper.state().loading).toBe(false);
expect(wrapper.state().hotspots).toEqual(hotspots);
expect(wrapper.state().selectedHotspotKey).toBe(hotspots[0].key);
expect(wrapper.state().selectedHotspot).toBe(hotspots[0]);
expect(wrapper.state().securityCategories).toEqual({
cat1: { title: 'cat 1' }
});
@@ -219,35 +219,43 @@ it('should handle hotspot update', async () => {
const hotspots = [mockRawHotspot(), mockRawHotspot({ key })];
(getSecurityHotspots as jest.Mock).mockResolvedValueOnce({
hotspots,
paging: { total: 2 }
paging: { pageIndex: 1, total: 1252 }
});

const wrapper = shallowRender();

await waitAndUpdate(wrapper);
wrapper.setState({ hotspotsPageIndex: 2 });

wrapper
jest.clearAllMocks();
(getSecurityHotspots as jest.Mock)
.mockResolvedValueOnce({
hotspots: [mockRawHotspot()],
paging: { pageIndex: 1, total: 1251 }
})
.mockResolvedValueOnce({
hotspots: [mockRawHotspot()],
paging: { pageIndex: 2, total: 1251 }
});

const selectedHotspotIndex = wrapper
.state()
.hotspots.findIndex(h => h.key === wrapper.state().selectedHotspot?.key);

await wrapper
.find(SecurityHotspotsAppRenderer)
.props()
.onUpdateHotspot({ key, status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE });
.onUpdateHotspot(key);

expect(wrapper.state().hotspots[0]).toEqual(hotspots[0]);
expect(wrapper.state().hotspots[1]).toEqual({
...hotspots[1],
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
});
expect(getMeasures).toBeCalled();
expect(getSecurityHotspots).toHaveBeenCalledTimes(2);

await waitAndUpdate(wrapper);
const previousState = wrapper.state();
wrapper.instance().handleHotspotUpdate({
key: 'unknown',
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
});
await waitAndUpdate(wrapper);
expect(wrapper.state()).toEqual(previousState);
expect(wrapper.state().hotspots).toHaveLength(2);
expect(wrapper.state().hotspotsPageIndex).toBe(2);
expect(wrapper.state().hotspotsTotal).toBe(1251);
expect(
wrapper.state().hotspots.findIndex(h => h.key === wrapper.state().selectedHotspot?.key)
).toBe(selectedHotspotIndex);

expect(getMeasures).toBeCalled();
});

it('should handle status filter change', async () => {

+ 2
- 1
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx View File

@@ -51,7 +51,7 @@ it('should render correctly with hotspots', () => {
.dive()
).toMatchSnapshot();
expect(
shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspotKey: 'h2' })
shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspot: mockRawHotspot({ key: 'h2' }) })
.find(ScreenPositionHelper)
.dive()
).toMatchSnapshot();
@@ -89,6 +89,7 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
onShowAllHotspots={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{}}
selectedHotspot={undefined}
{...props}
/>
);

+ 21
- 55
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap View File

@@ -46,60 +46,10 @@ exports[`should render correctly with hotspots 1`] = `
<A11ySkipTarget
anchor="security_hotspots_main"
/>
<div
className="layout-page"
>
<div
className="sidebar"
>
<HotspotList
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
]
}
hotspotsTotal={2}
isStaticListOfHotspots={true}
loadingMore={false}
onHotspotClick={[MockFunction]}
onLoadMore={[MockFunction]}
securityCategories={Object {}}
statusFilter="TO_REVIEW"
/>
</div>
<div
className="main"
/>
</div>
<EmptyHotspotsPage
filtered={false}
isStaticListOfHotspots={true}
/>
</div>
</div>
`;
@@ -172,7 +122,23 @@ exports[`should render correctly with hotspots 2`] = `
onHotspotClick={[MockFunction]}
onLoadMore={[MockFunction]}
securityCategories={Object {}}
selectedHotspotKey="h2"
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
statusFilter="TO_REVIEW"
/>
</div>

+ 1
- 10
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts View File

@@ -125,19 +125,10 @@ describe('sortHotspots', () => {
});

describe('groupByCategory', () => {
it('should group and sort properly', () => {
it('should group properly', () => {
const result = groupByCategory(hotspots, categories);

expect(result).toHaveLength(7);
expect(result.map(g => g.key)).toEqual([
'xss',
'dos',
'log-injection',
'object-injection',
'ssrf',
'xxe',
'xpath-injection'
]);
});
});


+ 12
- 9
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx View File

@@ -25,17 +25,17 @@ import { RawHotspot } from '../../../types/security-hotspots';
import HotspotListItem from './HotspotListItem';

export interface HotspotCategoryProps {
categoryKey: string;
expanded: boolean;
hotspots: RawHotspot[];
onHotspotClick: (key: string) => void;
selectedHotspotKey: string | undefined;
startsExpanded: boolean;
onHotspotClick: (hotspot: RawHotspot) => void;
onToggleExpand: (categoryKey: string, value: boolean) => void;
selectedHotspot: RawHotspot;
title: string;
}

export default function HotspotCategory(props: HotspotCategoryProps) {
const { hotspots, selectedHotspotKey, startsExpanded, title } = props;

const [expanded, setExpanded] = React.useState(startsExpanded);
const { categoryKey, expanded, hotspots, selectedHotspot, title } = props;

if (hotspots.length < 1) {
return null;
@@ -46,9 +46,12 @@ export default function HotspotCategory(props: HotspotCategoryProps) {
return (
<div className={classNames('hotspot-category', risk)}>
<a
className="hotspot-category-header display-flex-space-between display-flex-center"
className={classNames(
'hotspot-category-header display-flex-space-between display-flex-center',
{ 'contains-selected-hotspot': selectedHotspot.securityCategory === categoryKey }
)}
href="#"
onClick={() => setExpanded(!expanded)}>
onClick={() => props.onToggleExpand(categoryKey, !expanded)}>
<strong className="flex-1">{title}</strong>
<span>
<span className="counter-badge">{hotspots.length}</span>
@@ -66,7 +69,7 @@ export default function HotspotCategory(props: HotspotCategoryProps) {
<HotspotListItem
hotspot={h}
onClick={props.onHotspotClick}
selected={h.key === selectedHotspotKey}
selected={h.key === selectedHotspot.key}
/>
</li>
))}

+ 2
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css View File

@@ -37,7 +37,8 @@
border-left: 4px solid;
}

.hotspot-category .hotspot-category-header:hover {
.hotspot-category .hotspot-category-header:hover,
.hotspot-category .hotspot-category-header.contains-selected-hotspot {
color: var(--blue);
}


+ 104
- 58
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx View File

@@ -28,81 +28,127 @@ import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils';
import HotspotCategory from './HotspotCategory';
import './HotspotList.css';

export interface HotspotListProps {
interface Props {
hotspots: RawHotspot[];
hotspotsTotal?: number;
isStaticListOfHotspots: boolean;
loadingMore: boolean;
onHotspotClick: (key: string) => void;
onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
selectedHotspot: RawHotspot;
statusFilter: HotspotStatusFilter;
}

export default function HotspotList(props: HotspotListProps) {
const {
hotspots,
hotspotsTotal,
isStaticListOfHotspots,
loadingMore,
securityCategories,
selectedHotspotKey,
statusFilter
} = props;

const groupedHotspots: Array<{
interface State {
expandedCategories: T.Dict<boolean>;
groupedHotspots: Array<{
risk: RiskExposure;
categories: Array<{ key: string; hotspots: RawHotspot[]; title: string }>;
}> = React.useMemo(() => {
}>;
}

export default class HotspotList extends React.Component<Props, State> {
constructor(props: Props) {
super(props);

this.state = {
expandedCategories: { [props.selectedHotspot.securityCategory]: true },
groupedHotspots: this.groupHotspots(props.hotspots, props.securityCategories)
};
}

componentDidUpdate(prevProps: Props) {
// Force open the category of selected hotspot
if (
this.props.selectedHotspot.securityCategory !== prevProps.selectedHotspot.securityCategory
) {
this.handleToggleCategory(this.props.selectedHotspot.securityCategory, true);
}

// Compute the hotspot tree from the list
if (
this.props.hotspots !== prevProps.hotspots ||
this.props.securityCategories !== prevProps.securityCategories
) {
const groupedHotspots = this.groupHotspots(
this.props.hotspots,
this.props.securityCategories
);
this.setState({ groupedHotspots });
}
}

groupHotspots = (hotspots: RawHotspot[], securityCategories: T.StandardSecurityCategories) => {
const risks = groupBy(hotspots, h => h.vulnerabilityProbability);

return RISK_EXPOSURE_LEVELS.map(risk => ({
risk,
categories: groupByCategory(risks[risk], securityCategories)
})).filter(risk => risk.categories.length > 0);
}, [hotspots, securityCategories]);
};

handleToggleCategory = (categoryKey: string, value: boolean) => {
this.setState(({ expandedCategories }) => ({
expandedCategories: { ...expandedCategories, [categoryKey]: value }
}));
};

render() {
const {
hotspots,
hotspotsTotal,
isStaticListOfHotspots,
loadingMore,
selectedHotspot,
statusFilter
} = this.props;

const { expandedCategories, groupedHotspots } = this.state;

return (
<div className="huge-spacer-bottom">
<h1 className="hotspot-list-header bordered-bottom">
<SecurityHotspotIcon className="spacer-right" />
{translateWithParameters(
isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`,
hotspots.length
)}
</h1>
<ul className="big-spacer-bottom">
{groupedHotspots.map((riskGroup, groupIndex) => (
<li className="big-spacer-bottom" key={riskGroup.risk}>
<div className="hotspot-risk-header little-spacer-left">
<span>{translate('hotspots.risk_exposure')}</span>
<div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}>
{translate('risk_exposure', riskGroup.risk)}
return (
<div className="huge-spacer-bottom">
<h1 className="hotspot-list-header bordered-bottom">
<SecurityHotspotIcon className="spacer-right" />
{translateWithParameters(
isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`,
hotspots.length
)}
</h1>
<ul className="big-spacer-bottom">
{groupedHotspots.map(riskGroup => (
<li className="big-spacer-bottom" key={riskGroup.risk}>
<div className="hotspot-risk-header little-spacer-left">
<span>{translate('hotspots.risk_exposure')}</span>
<div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}>
{translate('risk_exposure', riskGroup.risk)}
</div>
</div>
</div>
<ul>
{riskGroup.categories.map((cat, catIndex) => (
<li className="spacer-bottom" key={cat.key}>
<HotspotCategory
hotspots={cat.hotspots}
onHotspotClick={props.onHotspotClick}
selectedHotspotKey={selectedHotspotKey}
startsExpanded={groupIndex === 0 && catIndex === 0}
title={cat.title}
/>
</li>
))}
</ul>
</li>
))}
</ul>
<ListFooter
count={hotspots.length}
loadMore={!loadingMore ? props.onLoadMore : undefined}
loading={loadingMore}
total={hotspotsTotal}
/>
</div>
);
<ul>
{riskGroup.categories.map(cat => (
<li className="spacer-bottom" key={cat.key}>
<HotspotCategory
categoryKey={cat.key}
expanded={expandedCategories[cat.key]}
hotspots={cat.hotspots}
onHotspotClick={this.props.onHotspotClick}
onToggleExpand={this.handleToggleCategory}
selectedHotspot={selectedHotspot}
title={cat.title}
/>
</li>
))}
</ul>
</li>
))}
</ul>
<ListFooter
count={hotspots.length}
loadMore={!loadingMore ? this.props.onLoadMore : undefined}
loading={loadingMore}
total={hotspotsTotal}
/>
</div>
);
}
}

+ 2
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx View File

@@ -25,7 +25,7 @@ import { getStatusOptionFromStatusAndResolution } from '../utils';

export interface HotspotListItemProps {
hotspot: RawHotspot;
onClick: (key: string) => void;
onClick: (hotspot: RawHotspot) => void;
selected: boolean;
}

@@ -35,7 +35,7 @@ export default function HotspotListItem(props: HotspotListItemProps) {
<a
className={classNames('hotspot-item', { highlight: selected })}
href="#"
onClick={() => !selected && props.onClick(hotspot.key)}>
onClick={() => !selected && props.onClick(hotspot)}>
<div className="little-spacer-left">{hotspot.message}</div>
<div className="badge spacer-top">
{translate(

+ 3
- 7
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx View File

@@ -21,13 +21,13 @@
import * as React from 'react';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { BranchLike } from '../../../types/branch-like';
import { Hotspot, HotspotUpdate } from '../../../types/security-hotspots';
import { Hotspot } from '../../../types/security-hotspots';
import HotspotViewerRenderer from './HotspotViewerRenderer';

interface Props {
branchLike?: BranchLike;
hotspotKey: string;
onUpdateHotspot: (hotspot: HotspotUpdate) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
securityCategories: T.StandardSecurityCategories;
}

@@ -70,11 +70,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
handleHotspotUpdate = () => {
return this.fetchHotspot().then((hotspot?: Hotspot) => {
if (hotspot) {
this.props.onUpdateHotspot({
key: hotspot.key,
status: hotspot.status,
resolution: hotspot.resolution
});
return this.props.onUpdateHotspot(hotspot.key);
}
});
};

+ 1
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx View File

@@ -31,7 +31,7 @@ export interface HotspotViewerRendererProps {
branchLike?: BranchLike;
hotspot?: Hotspot;
loading: boolean;
onUpdateHotspot: () => void;
onUpdateHotspot: () => Promise<void>;
securityCategories: T.StandardSecurityCategories;
}


+ 26
- 7
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx View File

@@ -27,30 +27,49 @@ it('should render correctly', () => {
});

it('should render correctly with hotspots', () => {
const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
const securityCategory = 'command-injection';
const hotspots = [
mockRawHotspot({ key: 'h1', securityCategory }),
mockRawHotspot({ key: 'h2', securityCategory })
];
expect(shallowRender({ hotspots })).toMatchSnapshot();
expect(shallowRender({ hotspots, startsExpanded: false })).toMatchSnapshot('collapsed');
expect(shallowRender({ hotspots, expanded: false })).toMatchSnapshot('collapsed');
expect(
shallowRender({ categoryKey: securityCategory, hotspots, selectedHotspot: hotspots[0] })
).toMatchSnapshot('contains selected');
});

it('should handle collapse and expand', () => {
const wrapper = shallowRender({ hotspots: [mockRawHotspot()] });
const onToggleExpand = jest.fn();

const categoryKey = 'xss-injection';

const wrapper = shallowRender({
categoryKey,
expanded: true,
hotspots: [mockRawHotspot()],
onToggleExpand
});

wrapper.find('.hotspot-category-header').simulate('click');

expect(wrapper).toMatchSnapshot();
expect(onToggleExpand).toBeCalledWith(categoryKey, false);

wrapper.setProps({ expanded: false });
wrapper.find('.hotspot-category-header').simulate('click');

expect(wrapper).toMatchSnapshot();
expect(onToggleExpand).toBeCalledWith(categoryKey, true);
});

function shallowRender(props: Partial<HotspotCategoryProps> = {}) {
return shallow(
<HotspotCategory
categoryKey="xss-injection"
expanded={true}
hotspots={[]}
onHotspotClick={jest.fn()}
selectedHotspotKey=""
startsExpanded={true}
onToggleExpand={jest.fn()}
selectedHotspot={mockRawHotspot()}
title="Class Injection"
{...props}
/>

+ 44
- 23
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx View File

@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
import { HotspotStatusFilter, RiskExposure } from '../../../../types/security-hotspots';
import HotspotList, { HotspotListProps } from '../HotspotList';
import HotspotList from '../HotspotList';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
@@ -32,32 +32,53 @@ it('should render correctly when the list of hotspot is static', () => {
expect(shallowRender({ isStaticListOfHotspots: true })).toMatchSnapshot();
});

const hotspots = [
mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }),
mockRawHotspot({
key: 'h3',
securityCategory: 'cat1',
vulnerabilityProbability: RiskExposure.MEDIUM
}),
mockRawHotspot({
key: 'h4',
securityCategory: 'cat1',
vulnerabilityProbability: RiskExposure.MEDIUM
}),
mockRawHotspot({
key: 'h5',
securityCategory: 'cat2',
vulnerabilityProbability: RiskExposure.MEDIUM
})
];

it('should render correctly with hotspots', () => {
const hotspots = [
mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }),
mockRawHotspot({
key: 'h3',
securityCategory: 'cat1',
vulnerabilityProbability: RiskExposure.MEDIUM
}),
mockRawHotspot({
key: 'h4',
securityCategory: 'cat1',
vulnerabilityProbability: RiskExposure.MEDIUM
}),
mockRawHotspot({
key: 'h5',
securityCategory: 'cat2',
vulnerabilityProbability: RiskExposure.MEDIUM
})
];
expect(shallowRender({ hotspots })).toMatchSnapshot('no pagination');
expect(shallowRender({ hotspots, hotspotsTotal: 7 })).toMatchSnapshot('pagination');
});

function shallowRender(props: Partial<HotspotListProps> = {}) {
return shallow(
it('should update expanded categories correctly', () => {
const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] });

expect(wrapper.state().expandedCategories).toEqual({ cat2: true });

wrapper.setProps({ selectedHotspot: hotspots[1] });

expect(wrapper.state().expandedCategories).toEqual({ cat1: true, cat2: true });
});

it('should update grouped hotspots when the list changes', () => {
const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] });

wrapper.setProps({ hotspots: [mockRawHotspot()] });

expect(wrapper.state().groupedHotspots).toHaveLength(1);
expect(wrapper.state().groupedHotspots[0].categories).toHaveLength(1);
expect(wrapper.state().groupedHotspots[0].categories[0].hotspots).toHaveLength(1);
});

function shallowRender(props: Partial<HotspotList['props']> = {}) {
return shallow<HotspotList>(
<HotspotList
hotspots={[]}
isStaticListOfHotspots={false}
@@ -65,7 +86,7 @@ function shallowRender(props: Partial<HotspotListProps> = {}) {
onHotspotClick={jest.fn()}
onLoadMore={jest.fn()}
securityCategories={{}}
selectedHotspotKey="h2"
selectedHotspot={mockRawHotspot({ key: 'h2' })}
statusFilter={HotspotStatusFilter.TO_REVIEW}
{...props}
/>

+ 1
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx View File

@@ -34,7 +34,7 @@ it('should handle click', () => {

wrapper.simulate('click');

expect(onClick).toBeCalledWith(hotspot.key);
expect(onClick).toBeCalledWith(hotspot);
});

function shallowRender(props: Partial<HotspotListItemProps> = {}) {

+ 59
- 62
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap View File

@@ -1,34 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should handle collapse and expand 1`] = `
<div
className="hotspot-category HIGH"
>
<a
className="hotspot-category-header display-flex-space-between display-flex-center"
href="#"
onClick={[Function]}
>
<strong
className="flex-1"
>
Class Injection
</strong>
<span>
<span
className="counter-badge"
>
1
</span>
<ChevronDownIcon
className="big-spacer-left"
/>
</span>
</a>
</div>
`;

exports[`should handle collapse and expand 2`] = `
exports[`should render correctly with hotspots 1`] = `
<div
className="hotspot-category HIGH"
>
@@ -46,7 +18,7 @@ exports[`should handle collapse and expand 2`] = `
<span
className="counter-badge"
>
1
2
</span>
<ChevronUpIcon
className="big-spacer-left"
@@ -55,7 +27,32 @@ exports[`should handle collapse and expand 2`] = `
</a>
<ul>
<li
key="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
key="h1"
>
<HotspotListItem
hotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
onClick={[MockFunction]}
selected={false}
/>
</li>
<li
key="h2"
>
<HotspotListItem
hotspot={
@@ -63,7 +60,7 @@ exports[`should handle collapse and expand 2`] = `
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
@@ -83,7 +80,7 @@ exports[`should handle collapse and expand 2`] = `
</div>
`;

exports[`should render correctly with hotspots 1`] = `
exports[`should render correctly with hotspots: collapsed 1`] = `
<div
className="hotspot-category HIGH"
>
@@ -91,6 +88,34 @@ exports[`should render correctly with hotspots 1`] = `
className="hotspot-category-header display-flex-space-between display-flex-center"
href="#"
onClick={[Function]}
>
<strong
className="flex-1"
>
Class Injection
</strong>
<span>
<span
className="counter-badge"
>
2
</span>
<ChevronDownIcon
className="big-spacer-left"
/>
</span>
</a>
</div>
`;

exports[`should render correctly with hotspots: contains selected 1`] = `
<div
className="hotspot-category HIGH"
>
<a
className="hotspot-category-header display-flex-space-between display-flex-center contains-selected-hotspot"
href="#"
onClick={[Function]}
>
<strong
className="flex-1"
@@ -131,7 +156,7 @@ exports[`should render correctly with hotspots 1`] = `
}
}
onClick={[MockFunction]}
selected={false}
selected={true}
/>
</li>
<li
@@ -163,32 +188,4 @@ exports[`should render correctly with hotspots 1`] = `
</div>
`;

exports[`should render correctly with hotspots: collapsed 1`] = `
<div
className="hotspot-category HIGH"
>
<a
className="hotspot-category-header display-flex-space-between display-flex-center"
href="#"
onClick={[Function]}
>
<strong
className="flex-1"
>
Class Injection
</strong>
<span>
<span
className="counter-badge"
>
2
</span>
<ChevronDownIcon
className="big-spacer-left"
/>
</span>
</a>
</div>
`;

exports[`should render correctly: empty 1`] = `""`;

+ 168
- 32
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap View File

@@ -102,22 +102,23 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
<ul>
<li
className="spacer-bottom"
key="cat1"
key="cat2"
>
<HotspotCategory
categoryKey="cat2"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat1",
"securityCategory": "cat2",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
@@ -125,29 +126,46 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={true}
title="cat1"
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat2"
/>
</li>
<li
className="spacer-bottom"
key="cat2"
key="cat1"
>
<HotspotCategory
categoryKey="cat1"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h1",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat2",
"securityCategory": "cat1",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
@@ -155,9 +173,25 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={false}
title="cat2"
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat1"
/>
</li>
</ul>
@@ -184,6 +218,7 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
key="cat1"
>
<HotspotCategory
categoryKey="cat1"
hotspots={
Array [
Object {
@@ -219,8 +254,24 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={false}
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat1"
/>
</li>
@@ -229,6 +280,7 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
key="cat2"
>
<HotspotCategory
categoryKey="cat2"
hotspots={
Array [
Object {
@@ -249,8 +301,24 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={false}
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat2"
/>
</li>
@@ -299,22 +367,23 @@ exports[`should render correctly with hotspots: pagination 1`] = `
<ul>
<li
className="spacer-bottom"
key="cat1"
key="cat2"
>
<HotspotCategory
categoryKey="cat2"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat1",
"securityCategory": "cat2",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
@@ -322,29 +391,46 @@ exports[`should render correctly with hotspots: pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={true}
title="cat1"
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat2"
/>
</li>
<li
className="spacer-bottom"
key="cat2"
key="cat1"
>
<HotspotCategory
categoryKey="cat1"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h1",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat2",
"securityCategory": "cat1",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
@@ -352,9 +438,25 @@ exports[`should render correctly with hotspots: pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={false}
title="cat2"
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat1"
/>
</li>
</ul>
@@ -381,6 +483,7 @@ exports[`should render correctly with hotspots: pagination 1`] = `
key="cat1"
>
<HotspotCategory
categoryKey="cat1"
hotspots={
Array [
Object {
@@ -416,8 +519,24 @@ exports[`should render correctly with hotspots: pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={false}
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat1"
/>
</li>
@@ -426,6 +545,7 @@ exports[`should render correctly with hotspots: pagination 1`] = `
key="cat2"
>
<HotspotCategory
categoryKey="cat2"
hotspots={
Array [
Object {
@@ -446,8 +566,24 @@ exports[`should render correctly with hotspots: pagination 1`] = `
]
}
onHotspotClick={[MockFunction]}
selectedHotspotKey="h2"
startsExpanded={false}
onToggleExpand={[Function]}
selectedHotspot={
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
}
title="cat2"
/>
</li>

+ 7
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx View File

@@ -19,7 +19,9 @@
*/

import * as React from 'react';
import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { assignSecurityHotspot } from '../../../../api/security-hotspots';
import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage';
import { withCurrentUser } from '../../../../components/hoc/withCurrentUser';
import { isLoggedIn } from '../../../../helpers/users';
import { Hotspot, HotspotStatus } from '../../../../types/security-hotspots';
@@ -72,6 +74,11 @@ export class Assignee extends React.PureComponent<Props, State> {
this.props.onAssigneeChange();
}
})
.then(() =>
addGlobalSuccessMessage(
translateWithParameters('hotspots.assign.success', newAssignee.name)
)
)
.catch(() => this.setState({ loading: false }));
}
};

+ 6
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx View File

@@ -22,6 +22,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { assignSecurityHotspot } from '../../../../../api/security-hotspots';
import addGlobalSuccessMessage from '../../../../../app/utils/addGlobalSuccessMessage';
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockUser } from '../../../../../helpers/testMocks';
import { HotspotStatus } from '../../../../../types/security-hotspots';
@@ -32,6 +33,10 @@ jest.mock('../../../../../api/security-hotspots', () => ({
assignSecurityHotspot: jest.fn()
}));

jest.mock('../../../../../app/utils/addGlobalSuccessMessage', () => ({
default: jest.fn()
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(
@@ -78,6 +83,7 @@ it('should handle assign event correctly', async () => {
loading: false
});
expect(onAssigneeChange).toHaveBeenCalled();
expect(addGlobalSuccessMessage).toHaveBeenCalled();
});

function shallowRender(props?: Partial<Assignee['props']>) {

+ 10
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx View File

@@ -18,7 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage';
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
import {
getStatusAndResolutionFromStatusOption,
@@ -86,6 +88,14 @@ export default class StatusSelection extends React.PureComponent<Props, State> {
this.setState({ loading: false });
this.props.onStatusOptionChange(selectedStatus);
})
.then(() =>
addGlobalSuccessMessage(
translateWithParameters(
'hotspots.update.success',
translate('hotspots.status_option', selectedStatus)
)
)
)
.catch(() => this.setState({ loading: false }));
}
};

+ 5
- 8
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts View File

@@ -44,14 +44,11 @@ export function groupByCategory(
) {
const groups = groupBy(hotspots, h => h.securityCategory);

return sortBy(
Object.keys(groups).map(key => ({
key,
title: getCategoryTitle(key, securityCategories),
hotspots: groups[key]
})),
cat => cat.title
);
return Object.keys(groups).map(key => ({
key,
title: getCategoryTitle(key, securityCategories),
hotspots: groups[key]
}));
}

export function sortHotspots(

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -699,6 +699,9 @@ hotspot.filters.show_all=Show all hotspots
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot

hotspots.assign.success=Security Hotspot was successfully assigned to {0}
hotspots.update.success=Security Hotspot status was successfully changed to {0}

#------------------------------------------------------------------------------
#
# ISSUES

Loading…
Cancel
Save