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

@@ -154,7 +154,7 @@ th.hide-overflow {
}

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

.big-padded {

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

@@ -164,23 +164,23 @@ blockquote cite {

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

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

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

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

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

.zero-font-size {

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

@@ -347,6 +347,10 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
);
};

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

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

handleHotspotUpdate = (hotspotKey: string) => {
@@ -452,6 +456,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
onHotspotClick={this.handleHotspotClick}
onLoadMore={this.handleLoadMore}
onShowAllHotspots={this.handleShowAllHotspots}
onSwitchStatusFilter={this.handleChangeStatusFilter}
onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={standards[SecurityStandard.SONARSOURCE]}
selectedHotspot={selectedHotspot}

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

@@ -57,6 +57,7 @@ export interface SecurityHotspotsAppRendererProps {
onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
onShowAllHotspots: () => void;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
selectedHotspot: RawHotspot | undefined;
securityCategories: T.StandardSecurityCategories;
@@ -172,6 +173,8 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
branchLike={branchLike}
component={component}
hotspotKey={selectedHotspot.key}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
onSwitchStatusFilter={props.onSwitchStatusFilter}
onUpdateHotspot={props.onUpdateHotspot}
securityCategories={securityCategories}
/>

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

@@ -343,8 +343,8 @@ it('should handle status filter change', async () => {

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.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED })

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

@@ -144,6 +144,7 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
onHotspotClick={jest.fn()}
onLoadMore={jest.fn()}
onShowAllHotspots={jest.fn()}
onSwitchStatusFilter={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{}}
selectedHotspot={undefined}

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

@@ -49,6 +49,7 @@ exports[`should render correctly 1`] = `
onHotspotClick={[Function]}
onLoadMore={[Function]}
onShowAllHotspots={[Function]}
onSwitchStatusFilter={[Function]}
onUpdateHotspot={[Function]}
securityCategories={Object {}}
standards={

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

@@ -22,6 +22,7 @@ import { mockUser } from '../../../helpers/testMocks';
import {
HotspotResolution,
HotspotStatus,
HotspotStatusFilter,
HotspotStatusOption,
ReviewHistoryType,
RiskExposure
@@ -29,6 +30,7 @@ import {
import {
getHotspotReviewHistory,
getStatusAndResolutionFromStatusOption,
getStatusFilterFromStatusOption,
getStatusOptionFromStatusAndResolution,
groupByCategory,
mapRules,
@@ -260,3 +262,17 @@ describe('getStatusAndResolutionFromStatusOption', () => {
});
});
});

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

@@ -21,21 +21,30 @@ import * as React from 'react';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { scrollToElement } from '../../../helpers/scrolling';
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';

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

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

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

componentDidMount() {
@@ -79,10 +88,11 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
.catch(() => this.mounted && this.setState({ loading: false }));
};

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

if (statusUpdate) {
this.setState({ lastStatusChangedTo: statusOption, showStatusUpdateSuccessModal: true });
await this.props.onUpdateHotspot(hotspotKey);
} else {
await this.fetchHotspot();
@@ -106,9 +116,26 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
this.setState({ commentVisible: false });
};

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

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

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 (
<HotspotViewerRenderer
@@ -117,10 +144,15 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
commentTextRef={this.commentTextRef}
commentVisible={commentVisible}
hotspot={hotspot}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
lastStatusChangedTo={lastStatusChangedTo}
loading={loading}
onCloseComment={this.handleCloseComment}
onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal}
onOpenComment={this.handleOpenComment}
onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot}
onUpdateHotspot={this.handleHotspotUpdate}
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
securityCategories={securityCategories}
/>
);

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

@@ -34,7 +34,7 @@ import {
} from '../../../helpers/urls';
import { isLoggedIn } from '../../../helpers/users';
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 HotspotOpenInIdeButton from './HotspotOpenInIdeButton';
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
@@ -42,18 +42,24 @@ import HotspotSnippetContainer from './HotspotSnippetContainer';
import './HotspotViewer.css';
import HotspotViewerTabs from './HotspotViewerTabs';
import Status from './status/Status';
import StatusUpdateSuccessModal from './StatusUpdateSuccessModal';

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

@@ -63,7 +69,10 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
component,
currentUser,
hotspot,
hotspotsReviewedMeasure,
loading,
lastStatusChangedTo,
showStatusUpdateSuccessModal,
securityCategories,
commentTextRef,
commentVisible
@@ -79,6 +88,15 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {

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

{hotspot && (
<div className="big-padded hotspot-content">
<div className="huge-spacer-bottom display-flex-space-between">
@@ -142,7 +160,10 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
</div>
</div>
<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>


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

@@ -0,0 +1,102 @@
/*
* 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

@@ -24,6 +24,7 @@ import { getSecurityHotspotDetails } from '../../../../api/security-hotspots';
import { mockComponent } from '../../../../helpers/mocks/component';
import { scrollToElement } from '../../../../helpers/scrolling';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import HotspotViewer from '../HotspotViewer';
import HotspotViewerRenderer from '../HotspotViewerRenderer';

@@ -63,6 +64,36 @@ it('should refresh hotspot list on status update', () => {
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', () => {
const onUpdateHotspot = jest.fn();
const wrapper = shallowRender({ onUpdateHotspot });
@@ -118,6 +149,7 @@ function shallowRender(props?: Partial<HotspotViewer['props']>) {
<HotspotViewer
component={mockComponent()}
hotspotKey={hotspotKey}
onSwitchStatusFilter={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{ cat1: { title: 'cat1' } }}
{...props}

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

@@ -23,13 +23,17 @@ import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockUser } from '../../../../helpers/testMocks';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';
import Status from '../status/Status';

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

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: mockHotspot({ assignee: undefined }) })).toMatchSnapshot(
'unassigned'
@@ -47,6 +51,18 @@ it('should render correctly', () => {
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>) {
return shallow(
<HotspotViewerRenderer
@@ -56,11 +72,16 @@ function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
commentVisible={false}
currentUser={mockCurrentUser()}
hotspot={mockHotspot()}
hotspotsReviewedMeasure="75"
lastStatusChangedTo={HotspotStatusOption.FIXED}
loading={false}
onCloseComment={jest.fn()}
onCloseStatusUpdateSuccessModal={jest.fn()}
onOpenComment={jest.fn()}
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
showStatusUpdateSuccessModal={false}
{...props}
/>
);

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

@@ -0,0 +1,45 @@
/*
* 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

@@ -32,7 +32,9 @@ exports[`should render correctly 1`] = `
}
loading={true}
onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
onOpenComment={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
securityCategories={
Object {
@@ -41,6 +43,7 @@ exports[`should render correctly 1`] = `
},
}
}
showStatusUpdateSuccessModal={false}
/>
`;

@@ -81,7 +84,9 @@ exports[`should render correctly 2`] = `
}
loading={false}
onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
onOpenComment={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
securityCategories={
Object {
@@ -90,5 +95,6 @@ exports[`should render correctly 2`] = `
},
}
}
showStatusUpdateSuccessModal={false}
/>
`;

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

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

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

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

exports[`should render correctly: assignee without name 1`] = `
exports[`should render correctly: default 1`] = `
<DeferredSpinner
className="big-spacer-left big-spacer-top"
loading={false}
@@ -1200,8 +1200,8 @@ exports[`should render correctly: assignee without name 1`] = `
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
@@ -1280,8 +1280,8 @@ exports[`should render correctly: assignee without name 1`] = `
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
@@ -1378,8 +1378,8 @@ exports[`should render correctly: assignee without name 1`] = `
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
@@ -1451,8 +1451,8 @@ exports[`should render correctly: assignee without name 1`] = `
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
@@ -1535,8 +1535,8 @@ exports[`should render correctly: assignee without name 1`] = `
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
@@ -2151,6 +2151,548 @@ exports[`should render correctly: no hotspot 1`] = `
/>
`;

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`] = `
<DeferredSpinner
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

@@ -0,0 +1,110 @@
// 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

@@ -28,7 +28,7 @@ import DropdownIcon from '../../../../components/icons/DropdownIcon';
import { PopupPlacement } from '../../../../components/ui/popups';
import { translate } from '../../../../helpers/l10n';
import { isLoggedIn } from '../../../../helpers/users';
import { Hotspot } from '../../../../types/security-hotspots';
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
import { getStatusOptionFromStatusAndResolution } from '../../utils';
import StatusDescription from './StatusDescription';
import StatusSelection from './StatusSelection';
@@ -37,7 +37,7 @@ export interface StatusProps {
currentUser: T.CurrentUser;
hotspot: Hotspot;

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

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

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

@@ -19,8 +19,6 @@
*/
import * as React from 'react';
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 {
getStatusAndResolutionFromStatusOption,
@@ -88,14 +86,6 @@ export default class StatusSelection extends React.PureComponent<Props, State> {
await this.props.onStatusOptionChange(selectedStatus);
this.setState({ loading: false });
})
.then(() =>
addGlobalSuccessMessage(
translateWithParameters(
'hotspots.update.success',
translate('hotspots.status_option', selectedStatus)
)
)
)
.catch(() => this.setState({ loading: false }));
}
};

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

@@ -29,6 +29,7 @@ import {
Hotspot,
HotspotResolution,
HotspotStatus,
HotspotStatusFilter,
HotspotStatusOption,
RawHotspot,
ReviewHistoryElement,
@@ -181,3 +182,13 @@ const STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP = {
export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) {
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

@@ -209,3 +209,15 @@
.modal-foot input[type='button'] {
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

@@ -768,6 +768,12 @@ hotspots.status_option.SAFE=Safe
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
hotspots.get_permalink=Get Permalink
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.assignee.assigned_to_me=Assigned to me
@@ -785,7 +791,7 @@ hotspots.review_hotspot=Review Hotspot

hotspots.assign.success=Security Hotspot was successfully assigned to {0}
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