Browse Source

SONAR-14511 Improve Security Hotspot status change flow

tags/9.1.0.47736
Wouter Admiraal 2 years ago
parent
commit
0d1d5eb5c9
22 changed files with 1012 additions and 56 deletions
  1. 1
    1
      server/sonar-web/src/main/js/app/styles/init/misc.css
  2. 5
    5
      server/sonar-web/src/main/js/app/styles/init/type.css
  3. 5
    0
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  4. 3
    0
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
  5. 2
    2
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
  6. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  7. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
  8. 16
    0
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
  9. 37
    5
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
  10. 24
    3
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  11. 102
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx
  12. 32
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
  13. 23
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  14. 45
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/StatusUpdateSuccessModal-test.tsx
  15. 6
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  16. 565
    23
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  17. 110
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap
  18. 4
    4
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx
  19. 0
    10
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
  20. 11
    0
      server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
  21. 12
    0
      server/sonar-web/src/main/js/components/controls/Modal.css
  22. 7
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/src/main/js/app/styles/init/misc.css View File

} }


.padded { .padded {
padding: var(--gridSize);
padding: var(--gridSize) !important;
} }


.big-padded { .big-padded {

+ 5
- 5
server/sonar-web/src/main/js/app/styles/init/type.css View File



small, small,
.small { .small {
font-size: var(--smallFontSize);
font-size: var(--smallFontSize) !important;
} }


.medium { .medium {
font-size: var(--mediumFontSize);
font-size: var(--mediumFontSize) !important;
} }


.big { .big {
font-size: var(--bigFontSize);
font-size: var(--bigFontSize) !important;
} }


.huge { .huge {
font-size: var(--hugeFontSize);
font-size: var(--hugeFontSize) !important;
} }


.gigantic { .gigantic {
font-size: var(--giganticFontSize);
font-size: var(--giganticFontSize) !important;
} }


.zero-font-size { .zero-font-size {

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

); );
}; };


handleChangeStatusFilter = (status: HotspotStatusFilter) => {
this.handleChangeFilters({ status });
};

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


handleHotspotUpdate = (hotspotKey: string) => { handleHotspotUpdate = (hotspotKey: string) => {
onHotspotClick={this.handleHotspotClick} onHotspotClick={this.handleHotspotClick}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
onShowAllHotspots={this.handleShowAllHotspots} onShowAllHotspots={this.handleShowAllHotspots}
onSwitchStatusFilter={this.handleChangeStatusFilter}
onUpdateHotspot={this.handleHotspotUpdate} onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={standards[SecurityStandard.SONARSOURCE]} securityCategories={standards[SecurityStandard.SONARSOURCE]}
selectedHotspot={selectedHotspot} selectedHotspot={selectedHotspot}

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

onHotspotClick: (hotspot: RawHotspot) => void; onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void; onLoadMore: () => void;
onShowAllHotspots: () => void; onShowAllHotspots: () => void;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>; onUpdateHotspot: (hotspotKey: string) => Promise<void>;
selectedHotspot: RawHotspot | undefined; selectedHotspot: RawHotspot | undefined;
securityCategories: T.StandardSecurityCategories; securityCategories: T.StandardSecurityCategories;
branchLike={branchLike} branchLike={branchLike}
component={component} component={component}
hotspotKey={selectedHotspot.key} hotspotKey={selectedHotspot.key}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
onSwitchStatusFilter={props.onSwitchStatusFilter}
onUpdateHotspot={props.onUpdateHotspot} onUpdateHotspot={props.onUpdateHotspot}
securityCategories={securityCategories} securityCategories={securityCategories}
/> />

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



expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]);


// Set filter to FIXED
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.FIXED });
// Set filter to FIXED (use the other method to check this one):
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilter.FIXED);


expect(getSecurityHotspots).toBeCalledWith( expect(getSecurityHotspots).toBeCalledWith(
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED })

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

onHotspotClick={jest.fn()} onHotspotClick={jest.fn()}
onLoadMore={jest.fn()} onLoadMore={jest.fn()}
onShowAllHotspots={jest.fn()} onShowAllHotspots={jest.fn()}
onSwitchStatusFilter={jest.fn()}
onUpdateHotspot={jest.fn()} onUpdateHotspot={jest.fn()}
securityCategories={{}} securityCategories={{}}
selectedHotspot={undefined} selectedHotspot={undefined}

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap View File

onHotspotClick={[Function]} onHotspotClick={[Function]}
onLoadMore={[Function]} onLoadMore={[Function]}
onShowAllHotspots={[Function]} onShowAllHotspots={[Function]}
onSwitchStatusFilter={[Function]}
onUpdateHotspot={[Function]} onUpdateHotspot={[Function]}
securityCategories={Object {}} securityCategories={Object {}}
standards={ standards={

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

import { import {
HotspotResolution, HotspotResolution,
HotspotStatus, HotspotStatus,
HotspotStatusFilter,
HotspotStatusOption, HotspotStatusOption,
ReviewHistoryType, ReviewHistoryType,
RiskExposure RiskExposure
import { import {
getHotspotReviewHistory, getHotspotReviewHistory,
getStatusAndResolutionFromStatusOption, getStatusAndResolutionFromStatusOption,
getStatusFilterFromStatusOption,
getStatusOptionFromStatusAndResolution, getStatusOptionFromStatusAndResolution,
groupByCategory, groupByCategory,
mapRules, mapRules,
}); });
}); });
}); });

describe('getStatusFilterFromStatusOption', () => {
it('should return the correct values', () => {
expect(getStatusFilterFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual(
HotspotStatusFilter.TO_REVIEW
);
expect(getStatusFilterFromStatusOption(HotspotStatusOption.SAFE)).toEqual(
HotspotStatusFilter.SAFE
);
expect(getStatusFilterFromStatusOption(HotspotStatusOption.FIXED)).toEqual(
HotspotStatusFilter.FIXED
);
});
});

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

import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { scrollToElement } from '../../../helpers/scrolling'; import { scrollToElement } from '../../../helpers/scrolling';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import {
Hotspot,
HotspotStatusFilter,
HotspotStatusOption
} from '../../../types/security-hotspots';
import { getStatusFilterFromStatusOption } from '../utils';
import HotspotViewerRenderer from './HotspotViewerRenderer'; import HotspotViewerRenderer from './HotspotViewerRenderer';


interface Props { interface Props {
branchLike?: BranchLike; branchLike?: BranchLike;
component: T.Component; component: T.Component;
hotspotKey: string; hotspotKey: string;
hotspotsReviewedMeasure?: string;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>; onUpdateHotspot: (hotspotKey: string) => Promise<void>;
securityCategories: T.StandardSecurityCategories; securityCategories: T.StandardSecurityCategories;
} }


interface State { interface State {
hotspot?: Hotspot; hotspot?: Hotspot;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean; loading: boolean;
commentVisible: boolean; commentVisible: boolean;
showStatusUpdateSuccessModal: boolean;
} }


export default class HotspotViewer extends React.PureComponent<Props, State> { export default class HotspotViewer extends React.PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.commentTextRef = React.createRef<HTMLTextAreaElement>(); this.commentTextRef = React.createRef<HTMLTextAreaElement>();
this.state = { loading: false, commentVisible: false };
this.state = { loading: false, commentVisible: false, showStatusUpdateSuccessModal: false };
} }


componentDidMount() { componentDidMount() {
.catch(() => this.mounted && this.setState({ loading: false })); .catch(() => this.mounted && this.setState({ loading: false }));
}; };


handleHotspotUpdate = async (statusUpdate = false) => {
handleHotspotUpdate = async (statusUpdate = false, statusOption?: HotspotStatusOption) => {
const { hotspotKey } = this.props; const { hotspotKey } = this.props;


if (statusUpdate) { if (statusUpdate) {
this.setState({ lastStatusChangedTo: statusOption, showStatusUpdateSuccessModal: true });
await this.props.onUpdateHotspot(hotspotKey); await this.props.onUpdateHotspot(hotspotKey);
} else { } else {
await this.fetchHotspot(); await this.fetchHotspot();
this.setState({ commentVisible: false }); this.setState({ commentVisible: false });
}; };


handleSwitchFilterToStatusOfUpdatedHotspot = () => {
const { lastStatusChangedTo } = this.state;
if (lastStatusChangedTo) {
this.props.onSwitchStatusFilter(getStatusFilterFromStatusOption(lastStatusChangedTo));
}
};

handleCloseStatusUpdateSuccessModal = () => {
this.setState({ showStatusUpdateSuccessModal: false });
};

render() { render() {
const { branchLike, component, securityCategories } = this.props;
const { hotspot, loading, commentVisible } = this.state;
const { branchLike, component, hotspotsReviewedMeasure, securityCategories } = this.props;
const {
hotspot,
lastStatusChangedTo,
loading,
commentVisible,
showStatusUpdateSuccessModal
} = this.state;


return ( return (
<HotspotViewerRenderer <HotspotViewerRenderer
commentTextRef={this.commentTextRef} commentTextRef={this.commentTextRef}
commentVisible={commentVisible} commentVisible={commentVisible}
hotspot={hotspot} hotspot={hotspot}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
lastStatusChangedTo={lastStatusChangedTo}
loading={loading} loading={loading}
onCloseComment={this.handleCloseComment} onCloseComment={this.handleCloseComment}
onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal}
onOpenComment={this.handleOpenComment} onOpenComment={this.handleOpenComment}
onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot}
onUpdateHotspot={this.handleHotspotUpdate} onUpdateHotspot={this.handleHotspotUpdate}
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
securityCategories={securityCategories} securityCategories={securityCategories}
/> />
); );

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

} from '../../../helpers/urls'; } from '../../../helpers/urls';
import { isLoggedIn } from '../../../helpers/users'; import { isLoggedIn } from '../../../helpers/users';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots';
import Assignee from './assignee/Assignee'; import Assignee from './assignee/Assignee';
import HotspotOpenInIdeButton from './HotspotOpenInIdeButton'; import HotspotOpenInIdeButton from './HotspotOpenInIdeButton';
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments'; import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
import './HotspotViewer.css'; import './HotspotViewer.css';
import HotspotViewerTabs from './HotspotViewerTabs'; import HotspotViewerTabs from './HotspotViewerTabs';
import Status from './status/Status'; import Status from './status/Status';
import StatusUpdateSuccessModal from './StatusUpdateSuccessModal';


export interface HotspotViewerRendererProps { export interface HotspotViewerRendererProps {
branchLike?: BranchLike; branchLike?: BranchLike;
component: T.Component; component: T.Component;
currentUser: T.CurrentUser; currentUser: T.CurrentUser;
hotspot?: Hotspot; hotspot?: Hotspot;
hotspotsReviewedMeasure?: string;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean; loading: boolean;
commentVisible: boolean; commentVisible: boolean;
commentTextRef: React.RefObject<HTMLTextAreaElement>; commentTextRef: React.RefObject<HTMLTextAreaElement>;
onOpenComment: () => void; onOpenComment: () => void;
onCloseComment: () => void; onCloseComment: () => void;
onUpdateHotspot: (statusUpdate?: boolean) => Promise<void>;
onCloseStatusUpdateSuccessModal: () => void;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
onSwitchFilterToStatusOfUpdatedHotspot: () => void;
showStatusUpdateSuccessModal: boolean;
securityCategories: T.StandardSecurityCategories; securityCategories: T.StandardSecurityCategories;
} }


component, component,
currentUser, currentUser,
hotspot, hotspot,
hotspotsReviewedMeasure,
loading, loading,
lastStatusChangedTo,
showStatusUpdateSuccessModal,
securityCategories, securityCategories,
commentTextRef, commentTextRef,
commentVisible commentVisible


return ( return (
<DeferredSpinner className="big-spacer-left big-spacer-top" loading={loading}> <DeferredSpinner className="big-spacer-left big-spacer-top" loading={loading}>
{showStatusUpdateSuccessModal && (
<StatusUpdateSuccessModal
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
lastStatusChangedTo={lastStatusChangedTo}
onClose={props.onCloseStatusUpdateSuccessModal}
onSwitchFilterToStatusOfUpdatedHotspot={props.onSwitchFilterToStatusOfUpdatedHotspot}
/>
)}

{hotspot && ( {hotspot && (
<div className="big-padded hotspot-content"> <div className="big-padded hotspot-content">
<div className="huge-spacer-bottom display-flex-space-between"> <div className="huge-spacer-bottom display-flex-space-between">
</div> </div>
</div> </div>
<div className="huge-spacer-left abs-width-400"> <div className="huge-spacer-left abs-width-400">
<Status hotspot={hotspot} onStatusChange={() => props.onUpdateHotspot(true)} />
<Status
hotspot={hotspot}
onStatusChange={statusOption => props.onUpdateHotspot(true, statusOption)}
/>
</div> </div>
</div> </div>



+ 102
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx View File

/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as classNames from 'classnames';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, ButtonLink } from '../../../components/controls/buttons';
import Modal from '../../../components/controls/Modal';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { HotspotStatusOption } from '../../../types/security-hotspots';

export interface StatusUpdateSuccessModalProps {
hotspotsReviewedMeasure?: string;
lastStatusChangedTo?: HotspotStatusOption;
onClose: () => void;
onSwitchFilterToStatusOfUpdatedHotspot: () => void;
}

export default function StatusUpdateSuccessModal(props: StatusUpdateSuccessModalProps) {
const { hotspotsReviewedMeasure, lastStatusChangedTo } = props;

if (!lastStatusChangedTo) {
return null;
}

const closingHotspots = lastStatusChangedTo !== HotspotStatusOption.TO_REVIEW;
const statusLabel = translate('hotspots.status_option', lastStatusChangedTo);
const modalTitle = closingHotspots
? translate('hotspots.congratulations')
: translate('hotspots.update.success');

return (
<Modal contentLabel={modalTitle}>
<div className="modal-head">
<h2
className={classNames('huge text-normal', {
'text-success': closingHotspots
})}>
{modalTitle}
</h2>
</div>

<div className="modal-body">
<FormattedMessage
id="hotspots.successfully_changed_to_x"
defaultMessage={translate('hotspots.successfully_changed_to_x')}
values={{
status_label: statusLabel,
status_change: (
<strong>
{translateWithParameters('hotspots.successful_status_change_to_x', statusLabel)}
</strong>
)
}}
/>
{closingHotspots && (
<p className="spacer-top">
<FormattedMessage
id="hotspots.x_done_keep_going"
defaultMessage={translate('hotspots.x_done_keep_going')}
values={{
percentage: (
<strong>
{formatMeasure(hotspotsReviewedMeasure, 'PERCENT', {
omitExtraDecimalZeros: true
})}
</strong>
)
}}
/>
</p>
)}
</div>

<div className="modal-foot modal-foot-clear display-flex-center display-flex-space-between">
<ButtonLink onClick={props.onSwitchFilterToStatusOfUpdatedHotspot}>
{translateWithParameters('hotspots.see_x_hotspots', statusLabel)}
</ButtonLink>
<Button className="button-primary padded" onClick={props.onClose}>
{translate('hotspots.continue_to_next_hotspot')}
</Button>
</div>
</Modal>
);
}

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

import { mockComponent } from '../../../../helpers/mocks/component'; import { mockComponent } from '../../../../helpers/mocks/component';
import { scrollToElement } from '../../../../helpers/scrolling'; import { scrollToElement } from '../../../../helpers/scrolling';
import { waitAndUpdate } from '../../../../helpers/testUtils'; import { waitAndUpdate } from '../../../../helpers/testUtils';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import HotspotViewer from '../HotspotViewer'; import HotspotViewer from '../HotspotViewer';
import HotspotViewerRenderer from '../HotspotViewerRenderer'; import HotspotViewerRenderer from '../HotspotViewerRenderer';


expect(onUpdateHotspot).toHaveBeenCalled(); expect(onUpdateHotspot).toHaveBeenCalled();
}); });


it('should store last status selected when updating a hotspot status', () => {
const wrapper = shallowRender();

expect(wrapper.state().lastStatusChangedTo).toBeUndefined();
wrapper
.find(HotspotViewerRenderer)
.props()
.onUpdateHotspot(true, HotspotStatusOption.FIXED);
expect(wrapper.state().lastStatusChangedTo).toBe(HotspotStatusOption.FIXED);
});

it('should correctly propagate a request to switch the status filter', () => {
const onSwitchStatusFilter = jest.fn();
const wrapper = shallowRender({ onSwitchStatusFilter });

wrapper.instance().handleSwitchFilterToStatusOfUpdatedHotspot();
expect(onSwitchStatusFilter).not.toBeCalled();

wrapper.setState({ lastStatusChangedTo: HotspotStatusOption.FIXED });
wrapper.instance().handleSwitchFilterToStatusOfUpdatedHotspot();
expect(onSwitchStatusFilter).toBeCalledWith(HotspotStatusOption.FIXED);
});

it('should correctly close the success modal', () => {
const wrapper = shallowRender();
wrapper.setState({ showStatusUpdateSuccessModal: true });
wrapper.instance().handleCloseStatusUpdateSuccessModal();
expect(wrapper.state().showStatusUpdateSuccessModal).toBe(false);
});

it('should NOT refresh hotspot list on assignee/comment updates', () => { it('should NOT refresh hotspot list on assignee/comment updates', () => {
const onUpdateHotspot = jest.fn(); const onUpdateHotspot = jest.fn();
const wrapper = shallowRender({ onUpdateHotspot }); const wrapper = shallowRender({ onUpdateHotspot });
<HotspotViewer <HotspotViewer
component={mockComponent()} component={mockComponent()}
hotspotKey={hotspotKey} hotspotKey={hotspotKey}
onSwitchStatusFilter={jest.fn()}
onUpdateHotspot={jest.fn()} onUpdateHotspot={jest.fn()}
securityCategories={{ cat1: { title: 'cat1' } }} securityCategories={{ cat1: { title: 'cat1' } }}
{...props} {...props}

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

import { mockComponent } from '../../../../helpers/mocks/component'; import { mockComponent } from '../../../../helpers/mocks/component';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockUser } from '../../../../helpers/testMocks'; import { mockCurrentUser, mockUser } from '../../../../helpers/testMocks';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';
import Status from '../status/Status';


jest.mock('../../../../helpers/users', () => ({ isLoggedIn: jest.fn(() => true) })); jest.mock('../../../../helpers/users', () => ({ isLoggedIn: jest.fn(() => true) }));


it('should render correctly', () => { it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ showStatusUpdateSuccessModal: true })).toMatchSnapshot(
'show success modal'
);
expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot'); expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot');
expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot( expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot(
'unassigned' 'unassigned'
expect(shallowRender()).toMatchSnapshot('anonymous user'); expect(shallowRender()).toMatchSnapshot('anonymous user');
}); });


it('correctly propagates the status change', () => {
const onUpdateHotspot = jest.fn();
const wrapper = shallowRender({ onUpdateHotspot });

wrapper
.find(Status)
.props()
.onStatusChange(HotspotStatusOption.FIXED);

expect(onUpdateHotspot).toHaveBeenCalledWith(true, HotspotStatusOption.FIXED);
});

function shallowRender(props?: Partial<HotspotViewerRendererProps>) { function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow( return shallow(
<HotspotViewerRenderer <HotspotViewerRenderer
commentVisible={false} commentVisible={false}
currentUser={mockCurrentUser()} currentUser={mockCurrentUser()}
hotspot={mockHotspot()} hotspot={mockHotspot()}
hotspotsReviewedMeasure="75"
lastStatusChangedTo={HotspotStatusOption.FIXED}
loading={false} loading={false}
onCloseComment={jest.fn()} onCloseComment={jest.fn()}
onCloseStatusUpdateSuccessModal={jest.fn()}
onOpenComment={jest.fn()} onOpenComment={jest.fn()}
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()}
onUpdateHotspot={jest.fn()} onUpdateHotspot={jest.fn()}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }} securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
showStatusUpdateSuccessModal={false}
{...props} {...props}
/> />
); );

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

/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { shallow } from 'enzyme';
import * as React from 'react';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import StatusUpdateSuccessModal, {
StatusUpdateSuccessModalProps
} from '../StatusUpdateSuccessModal';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ lastStatusChangedTo: HotspotStatusOption.TO_REVIEW })).toMatchSnapshot(
'opening hotspots again'
);
expect(shallowRender({ lastStatusChangedTo: undefined }).type()).toBeNull();
});

function shallowRender(props: Partial<StatusUpdateSuccessModalProps> = {}) {
return shallow<StatusUpdateSuccessModalProps>(
<StatusUpdateSuccessModal
onClose={jest.fn()}
lastStatusChangedTo={HotspotStatusOption.FIXED}
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()}
{...props}
/>
);
}

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

} }
loading={true} loading={true}
onCloseComment={[Function]} onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
onOpenComment={[Function]} onOpenComment={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]} onUpdateHotspot={[Function]}
securityCategories={ securityCategories={
Object { Object {
}, },
} }
} }
showStatusUpdateSuccessModal={false}
/> />
`; `;


} }
loading={false} loading={false}
onCloseComment={[Function]} onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
onOpenComment={[Function]} onOpenComment={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]} onUpdateHotspot={[Function]}
securityCategories={ securityCategories={
Object { Object {
}, },
} }
} }
showStatusUpdateSuccessModal={false}
/> />
`; `;

+ 565
- 23
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP


exports[`should render correctly 1`] = `
exports[`should render correctly: anonymous user 1`] = `
<DeferredSpinner <DeferredSpinner
className="big-spacer-left big-spacer-top" className="big-spacer-left big-spacer-top"
loading={false} loading={false}
</DeferredSpinner> </DeferredSpinner>
`; `;


exports[`should render correctly: anonymous user 1`] = `
exports[`should render correctly: assignee without name 1`] = `
<DeferredSpinner <DeferredSpinner
className="big-spacer-left big-spacer-top" className="big-spacer-left big-spacer-top"
loading={false} loading={false}
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee",
"name": "John Doe",
"login": "assignee_login",
"name": undefined,
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee",
"name": "John Doe",
"login": "assignee_login",
"name": undefined,
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee",
"name": "John Doe",
"login": "assignee_login",
"name": undefined,
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee",
"name": "John Doe",
"login": "assignee_login",
"name": undefined,
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee",
"name": "John Doe",
"login": "assignee_login",
"name": undefined,
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
</DeferredSpinner> </DeferredSpinner>
`; `;


exports[`should render correctly: assignee without name 1`] = `
exports[`should render correctly: default 1`] = `
<DeferredSpinner <DeferredSpinner
className="big-spacer-left big-spacer-top" className="big-spacer-left big-spacer-top"
loading={false} loading={false}
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
"assigneeUser": Object { "assigneeUser": Object {
"active": true, "active": true,
"local": true, "local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
}, },
"author": "author", "author": "author",
"authorUser": Object { "authorUser": Object {
/> />
`; `;


exports[`should render correctly: show success modal 1`] = `
<DeferredSpinner
className="big-spacer-left big-spacer-top"
loading={false}
>
<StatusUpdateSuccessModal
hotspotsReviewedMeasure="75"
lastStatusChangedTo="FIXED"
onClose={[MockFunction]}
onSwitchFilterToStatusOfUpdatedHotspot={[MockFunction]}
/>
<div
className="big-padded hotspot-content"
>
<div
className="huge-spacer-bottom display-flex-space-between"
>
<div
className="display-flex-column"
>
<strong
className="big big-spacer-right little-spacer-bottom"
>
'3' is a magic number.
</strong>
<div>
<span
className="note padded-right"
>
That rule
</span>
<Link
className="small"
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to={
Object {
"pathname": "/coding_rules",
"query": Object {
"open": "squid:S2077",
"rule_key": "squid:S2077",
},
}
}
>
squid:S2077
</Link>
</div>
</div>
<div
className="display-flex-row flex-0"
>
<div
className="dropdown spacer-right flex-1-0-auto"
>
<Button
className="it__hs-add-comment"
onClick={[MockFunction]}
>
hotspots.comment.open
</Button>
</div>
<div
className="dropdown spacer-right flex-1-0-auto"
>
<HotspotOpenInIdeButton
hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
projectKey="hotspot-component"
/>
</div>
<ClipboardButton
className="flex-1-0-auto"
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
>
<LinkIcon
className="spacer-right"
/>
<span>
hotspots.get_permalink
</span>
</ClipboardButton>
</div>
</div>
<div
className="huge-spacer-bottom display-flex-row display-flex-space-between"
>
<div
className="hotspot-information display-flex-column display-flex-space-between"
>
<div
className="display-flex-center"
>
<span
className="big-spacer-right"
>
category
</span>
<strong
className="nowrap"
>
SQL injection
</strong>
</div>
<div
className="display-flex-center"
>
<span
className="big-spacer-right"
>
hotspots.risk_exposure
</span>
<div
className="hotspot-risk-badge HIGH"
>
risk_exposure.HIGH
</div>
</div>
<div
className="display-flex-center it__hs-assignee"
>
<span
className="big-spacer-right"
>
assignee
</span>
<div>
<Connect(withCurrentUser(Assignee))
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
}
onAssigneeChange={[MockFunction]}
/>
</div>
</div>
</div>
<div
className="huge-spacer-left abs-width-400"
>
<Connect(withCurrentUser(Status))
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
}
onStatusChange={[Function]}
/>
</div>
</div>
<HotspotSnippetContainer
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
}
/>
<HotspotViewerTabs
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
}
/>
<HotspotReviewHistoryAndComments
commentTextRef={
Object {
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
}
}
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>
`;

exports[`should render correctly: unassigned 1`] = ` exports[`should render correctly: unassigned 1`] = `
<DeferredSpinner <DeferredSpinner
className="big-spacer-left big-spacer-top" className="big-spacer-left big-spacer-top"

+ 110
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap View File

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<Modal
contentLabel="hotspots.congratulations"
>
<div
className="modal-head"
>
<h2
className="huge text-normal text-success"
>
hotspots.congratulations
</h2>
</div>
<div
className="modal-body"
>
<FormattedMessage
defaultMessage="hotspots.successfully_changed_to_x"
id="hotspots.successfully_changed_to_x"
values={
Object {
"status_change": <strong>
hotspots.successful_status_change_to_x.hotspots.status_option.FIXED
</strong>,
"status_label": "hotspots.status_option.FIXED",
}
}
/>
<p
className="spacer-top"
>
<FormattedMessage
defaultMessage="hotspots.x_done_keep_going"
id="hotspots.x_done_keep_going"
values={
Object {
"percentage": <strong>
</strong>,
}
}
/>
</p>
</div>
<div
className="modal-foot modal-foot-clear display-flex-center display-flex-space-between"
>
<ButtonLink
onClick={[MockFunction]}
>
hotspots.see_x_hotspots.hotspots.status_option.FIXED
</ButtonLink>
<Button
className="button-primary padded"
onClick={[MockFunction]}
>
hotspots.continue_to_next_hotspot
</Button>
</div>
</Modal>
`;

exports[`should render correctly: opening hotspots again 1`] = `
<Modal
contentLabel="hotspots.update.success"
>
<div
className="modal-head"
>
<h2
className="huge text-normal"
>
hotspots.update.success
</h2>
</div>
<div
className="modal-body"
>
<FormattedMessage
defaultMessage="hotspots.successfully_changed_to_x"
id="hotspots.successfully_changed_to_x"
values={
Object {
"status_change": <strong>
hotspots.successful_status_change_to_x.hotspots.status_option.TO_REVIEW
</strong>,
"status_label": "hotspots.status_option.TO_REVIEW",
}
}
/>
</div>
<div
className="modal-foot modal-foot-clear display-flex-center display-flex-space-between"
>
<ButtonLink
onClick={[MockFunction]}
>
hotspots.see_x_hotspots.hotspots.status_option.TO_REVIEW
</ButtonLink>
<Button
className="button-primary padded"
onClick={[MockFunction]}
>
hotspots.continue_to_next_hotspot
</Button>
</div>
</Modal>
`;

+ 4
- 4
server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx View File

import { PopupPlacement } from '../../../../components/ui/popups'; import { PopupPlacement } from '../../../../components/ui/popups';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { isLoggedIn } from '../../../../helpers/users'; import { isLoggedIn } from '../../../../helpers/users';
import { Hotspot } from '../../../../types/security-hotspots';
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
import { getStatusOptionFromStatusAndResolution } from '../../utils'; import { getStatusOptionFromStatusAndResolution } from '../../utils';
import StatusDescription from './StatusDescription'; import StatusDescription from './StatusDescription';
import StatusSelection from './StatusSelection'; import StatusSelection from './StatusSelection';
currentUser: T.CurrentUser; currentUser: T.CurrentUser;
hotspot: Hotspot; hotspot: Hotspot;


onStatusChange: () => Promise<void>;
onStatusChange: (statusOption: HotspotStatusOption) => Promise<void>;
} }


export function Status(props: StatusProps) { export function Status(props: StatusProps) {
<DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}> <DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}>
<StatusSelection <StatusSelection
hotspot={hotspot} hotspot={hotspot}
onStatusOptionChange={async () => {
await props.onStatusChange();
onStatusOptionChange={async status => {
await props.onStatusChange(status);
setIsOpen(false); setIsOpen(false);
}} }}
/> />

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

*/ */
import * as React from 'react'; import * as React from 'react';
import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
import { import {
getStatusAndResolutionFromStatusOption, getStatusAndResolutionFromStatusOption,
await this.props.onStatusOptionChange(selectedStatus); await this.props.onStatusOptionChange(selectedStatus);
this.setState({ loading: false }); this.setState({ loading: false });
}) })
.then(() =>
addGlobalSuccessMessage(
translateWithParameters(
'hotspots.update.success',
translate('hotspots.status_option', selectedStatus)
)
)
)
.catch(() => this.setState({ loading: false })); .catch(() => this.setState({ loading: false }));
} }
}; };

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

Hotspot, Hotspot,
HotspotResolution, HotspotResolution,
HotspotStatus, HotspotStatus,
HotspotStatusFilter,
HotspotStatusOption, HotspotStatusOption,
RawHotspot, RawHotspot,
ReviewHistoryElement, ReviewHistoryElement,
export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) { export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) {
return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption]; return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption];
} }

const STATUS_OPTION_TO_STATUS_FILTER = {
[HotspotStatusOption.TO_REVIEW]: HotspotStatusFilter.TO_REVIEW,
[HotspotStatusOption.FIXED]: HotspotStatusFilter.FIXED,
[HotspotStatusOption.SAFE]: HotspotStatusFilter.SAFE
};

export function getStatusFilterFromStatusOption(statusOption: HotspotStatusOption) {
return STATUS_OPTION_TO_STATUS_FILTER[statusOption];
}

+ 12
- 0
server/sonar-web/src/main/js/components/controls/Modal.css View File

.modal-foot input[type='button'] { .modal-foot input[type='button'] {
margin-left: var(--gridSize); margin-left: var(--gridSize);
} }

.modal-foot button:first-of-type,
.modal-foot .button:first-of-type,
.modal-foot input[type='submit']:first-of-type,
.modal-foot input[type='button']:first-of-type {
margin-left: 0;
}

.modal-foot-clear {
border-top: 0;
background-color: transparent;
}

+ 7
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
hotspots.get_permalink=Get Permalink hotspots.get_permalink=Get Permalink
hotspots.no_associated_lines=Security Hotspot raised on the following file: hotspots.no_associated_lines=Security Hotspot raised on the following file:
hotspots.congratulations=Congratulations!
hotspots.successfully_changed_to_x=The Security Hotspot was {status_change}. You can find it by changing the top filter to display "{status_label}" Security Hotspots.
hotspots.successful_status_change_to_x=successfully changed to "{0}"
hotspots.x_done_keep_going={percentage} of the Security Hotspots have been reviewed, keep going!
hotspots.see_x_hotspots=See "{0}" Security Hotspots
hotspots.continue_to_next_hotspot=Continue reviewing next Security Hotspot


hotspot.filters.title=Filters hotspot.filters.title=Filters
hotspot.filters.assignee.assigned_to_me=Assigned to me hotspot.filters.assignee.assigned_to_me=Assigned to me


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


#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #

Loading…
Cancel
Save