Kaynağa Gözat

SONAR-12719 Move the status edition popup in hotspot main screen

tags/8.2.0.32929
Philippe Perrin 4 yıl önce
ebeveyn
işleme
9e025bf157
37 değiştirilmiş dosya ile 1838 ekleme ve 2321 silme
  1. 1
    1
      server/sonar-web/src/main/js/app/styles/init/icons.css
  2. 5
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  3. 49
    2
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
  4. 0
    77
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx
  5. 0
    163
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx
  6. 0
    166
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx
  7. 5
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
  8. 5
    16
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  9. 0
    78
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx
  10. 0
    155
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx
  11. 0
    109
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
  12. 2
    4
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  13. 0
    406
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap
  14. 0
    112
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
  15. 0
    544
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
  16. 2
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
  17. 2
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  18. 450
    441
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  19. 1
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx
  20. 4
    4
      server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
  21. 44
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css
  22. 107
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx
  23. 41
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
  24. 109
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
  25. 91
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
  26. 74
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
  27. 35
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
  28. 78
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
  29. 72
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
  30. 436
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
  31. 24
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
  32. 12
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
  33. 140
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
  34. 0
    7
      server/sonar-web/src/main/js/apps/security-hotspots/styles.css
  35. 35
    0
      server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
  36. 3
    3
      server/sonar-web/src/main/js/types/security-hotspots.ts
  37. 11
    27
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/src/main/js/app/styles/init/icons.css Dosyayı Görüntüle

@@ -77,7 +77,7 @@ a[class*=' icon-'] {
transition: opacity 0.3s ease;
}

a:hover > .icon-radio {
a:not(.disabled):hover > .icon-radio {
border-color: var(--blue);
}


+ 5
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css Dosyayı Görüntüle

@@ -384,6 +384,11 @@ th.huge-spacer-right {
justify-content: center;
}

.display-flex-justify-end {
display: flex !important;
justify-content: flex-end;
}

.display-flex-space-around {
display: flex !important;
justify-content: space-around;

+ 49
- 2
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts Dosyayı Görüntüle

@@ -19,8 +19,21 @@
*/
import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../helpers/testMocks';
import { ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots';
import { getHotspotReviewHistory, groupByCategory, mapRules, sortHotspots } from '../utils';
import {
HotspotResolution,
HotspotStatus,
HotspotStatusOption,
ReviewHistoryType,
RiskExposure
} from '../../../types/security-hotspots';
import {
getHotspotReviewHistory,
getStatusAndResolutionFromStatusOption,
getStatusOptionFromStatusAndResolution,
groupByCategory,
mapRules,
sortHotspots
} from '../utils';

const hotspots = [
mockRawHotspot({
@@ -223,3 +236,37 @@ describe('getHotspotReviewHistory', () => {
);
});
});

describe('getStatusOptionFromStatusAndResolution', () => {
it('should return the correct values', () => {
expect(
getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.FIXED)
).toBe(HotspotStatusOption.FIXED);
expect(
getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.SAFE)
).toBe(HotspotStatusOption.SAFE);
expect(getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED)).toBe(
HotspotStatusOption.FIXED
);
expect(getStatusOptionFromStatusAndResolution(HotspotStatus.TO_REVIEW)).toBe(
HotspotStatusOption.TO_REVIEW
);
});
});

describe('getStatusAndResolutionFromStatusOption', () => {
it('should return the correct values', () => {
expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual({
status: HotspotStatus.TO_REVIEW,
resolution: undefined
});
expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.FIXED)).toEqual({
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.FIXED
});
expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.SAFE)).toEqual({
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
});
});
});

+ 0
- 77
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx Dosyayı Görüntüle

@@ -1,77 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler';
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Hotspot } from '../../../types/security-hotspots';
import HotspotActionsForm from './HotspotActionsForm';

export interface HotspotActionsProps {
hotspot: Hotspot;
onSubmit: () => void;
}

const ESCAPE_KEY = 'Escape';

export default function HotspotActions(props: HotspotActionsProps) {
const { hotspot } = props;
const [open, setOpen] = React.useState(false);

React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === ESCAPE_KEY) {
setOpen(false);
}
};

document.addEventListener('keydown', handleKeyDown, false);

return () => {
document.removeEventListener('keydown', handleKeyDown, false);
};
});

return (
<div className="dropdown big-spacer-left flex-0">
<Button onClick={() => setOpen(!open)}>
{translate('hotspot.change_status', hotspot.status)}
<DropdownIcon className="little-spacer-left" />
</Button>

{open && (
<OutsideClickHandler onClickOutside={() => setOpen(false)}>
<DropdownOverlay placement={PopupPlacement.BottomRight}>
<HotspotActionsForm
hotspot={hotspot}
onSubmit={() => {
setOpen(false);
props.onSubmit();
}}
/>
</DropdownOverlay>
</OutsideClickHandler>
)}
</div>
);
}

+ 0
- 163
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx Dosyayı Görüntüle

@@ -1,163 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 React from 'react';
import {
assignSecurityHotspot,
commentSecurityHotspot,
setSecurityHotspotStatus
} from '../../../api/security-hotspots';
import {
Hotspot,
HotspotResolution,
HotspotStatus,
HotspotStatusOption
} from '../../../types/security-hotspots';
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer';

interface Props {
hotspot: Hotspot;
onSubmit: () => void;
}

interface State {
comment: string;
selectedOption: HotspotStatusOption;
selectedUser?: T.UserActive;
submitting: boolean;
}

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

let selectedOption = HotspotStatusOption.FIXED;
if (props.hotspot.status === HotspotStatus.TO_REVIEW) {
selectedOption = HotspotStatusOption.ADDITIONAL_REVIEW;
} else if (props.hotspot.resolution) {
selectedOption = HotspotStatusOption[props.hotspot.resolution];
}

this.state = {
comment: '',
selectedOption,
submitting: false
};
}

handleSelectOption = (selectedOption: HotspotStatusOption) => {
this.setState({ selectedOption });
};

handleAssign = (selectedUser: T.UserActive) => {
this.setState({ selectedUser });
};

handleCommentChange = (comment: string) => {
this.setState({ comment });
};

handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();

const { hotspot } = this.props;
const { comment, selectedOption, selectedUser } = this.state;

const status =
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW
? HotspotStatus.TO_REVIEW
: HotspotStatus.REVIEWED;

const resolution =
selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW
? HotspotResolution[selectedOption]
: undefined;

this.setState({ submitting: true });
/*
* updateAssignee depends on updateStatus, hence these are chained rather than
* run in parallel. The comment should also appear last in the changelog.
*/
return Promise.resolve()
.then(() => this.updateStatus(hotspot, status, resolution))
.then(() => this.updateAssignee(hotspot, selectedOption, selectedUser))
.then(() => this.addComment(hotspot, comment))
.then(() => {
this.props.onSubmit();
// No need to set "submitting", we are closing the window
})
.catch(() => {
this.setState({ submitting: false });
});
};

updateStatus = (hotspot: Hotspot, status: HotspotStatus, resolution?: HotspotResolution) => {
if (
hotspot.canChangeStatus &&
(status !== hotspot.status || resolution !== hotspot.resolution)
) {
return setSecurityHotspotStatus(hotspot.key, { status, resolution });
}

return Promise.resolve();
};

updateAssignee = (
hotspot: Hotspot,
selectedOption: HotspotStatusOption,
selectedUser?: T.UserActive
) => {
if (
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW &&
selectedUser &&
selectedUser.login !== hotspot.assignee
) {
return assignSecurityHotspot(hotspot.key, {
assignee: selectedUser.login
});
}
return Promise.resolve();
};

addComment = (hotspot: Hotspot, comment: string) => {
if (comment.length > 0) {
return commentSecurityHotspot(hotspot.key, comment);
}
return Promise.resolve();
};

render() {
const { hotspot } = this.props;
const { comment, selectedOption, selectedUser, submitting } = this.state;

return (
<HotspotActionsFormRenderer
comment={comment}
hotspot={hotspot}
onAssign={this.handleAssign}
onChangeComment={this.handleCommentChange}
onSelectOption={this.handleSelectOption}
onSubmit={this.handleSubmit}
selectedOption={selectedOption}
selectedUser={selectedUser}
submitting={submitting}
/>
);
}
}

+ 0
- 166
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx Dosyayı Görüntüle

@@ -1,166 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Radio from 'sonar-ui-common/components/controls/Radio';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate } from 'sonar-ui-common/helpers/l10n';
import MarkdownTips from '../../../components/common/MarkdownTips';
import {
Hotspot,
HotspotResolution,
HotspotStatus,
HotspotStatusOption
} from '../../../types/security-hotspots';

export interface HotspotActionsFormRendererProps {
comment: string;
hotspot: Hotspot;
onAssign: (user: T.UserActive) => void;
onChangeComment: (comment: string) => void;
onSelectOption: (option: HotspotStatusOption) => void;
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
selectedOption: HotspotStatusOption;
selectedUser?: T.UserActive;
submitting: boolean;
}

export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) {
const { comment, hotspot, selectedOption, submitting } = props;

const disableStatusChange = !hotspot.canChangeStatus;

return (
<form className="abs-width-400 padded" onSubmit={props.onSubmit}>
<h2>
{disableStatusChange
? translate('hotspots.form.title.disabled')
: translate('hotspots.form.title')}
</h2>
<div className="display-flex-column big-spacer-bottom">
{renderOption({
disabled: disableStatusChange,
option: HotspotStatusOption.FIXED,
selectedOption,
onClick: props.onSelectOption
})}
{renderOption({
disabled: disableStatusChange,
option: HotspotStatusOption.SAFE,
selectedOption,
onClick: props.onSelectOption
})}
{renderOption({
disabled: disableStatusChange,
option: HotspotStatusOption.ADDITIONAL_REVIEW,
selectedOption,
onClick: props.onSelectOption
})}
</div>
<div className="display-flex-column big-spacer-bottom">
<label className="little-spacer-bottom">{translate('hotspots.form.comment')}</label>
<textarea
autoFocus={true}
className="form-field fixed-width spacer-bottom"
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
props.onChangeComment(event.currentTarget.value)
}
placeholder={
selectedOption === HotspotStatusOption.SAFE
? translate('hotspots.form.comment.placeholder')
: ''
}
rows={6}
value={comment}
/>
<MarkdownTips />
</div>
<div className="text-right">
{submitting && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitting || !changes(props)}>
{translate('hotspots.form.submit', hotspot.status)}
</SubmitButton>
</div>
</form>
);
}

const noop = () => {};

function changes(params: {
comment: string;
hotspot: Hotspot;
selectedOption: HotspotStatusOption;
selectedUser?: T.UserActive;
}) {
const { comment, hotspot, selectedOption, selectedUser } = params;

const status =
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW
? HotspotStatus.TO_REVIEW
: HotspotStatus.REVIEWED;

const resolution =
selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW
? HotspotResolution[selectedOption]
: undefined;

return (
comment.length > 0 ||
selectedUser ||
status !== hotspot.status ||
resolution !== hotspot.resolution
);
}

function renderOption(params: {
disabled: boolean;
option: HotspotStatusOption;
onClick: (option: HotspotStatusOption) => void;
selectedOption: HotspotStatusOption;
}) {
const { disabled, onClick, option, selectedOption } = params;

const optionRender = (
<div className="big-spacer-top">
<Radio
checked={selectedOption === option}
className={classnames({ disabled })}
onCheck={disabled ? noop : onClick}
value={option}>
<h3 className={classnames({ 'text-muted': disabled })}>
{translate('hotspots.status_option', option)}
</h3>
</Radio>
<div className={classnames('radio-button-description', { 'text-muted': disabled })}>
{translate('hotspots.status_option', option, 'description')}
</div>
</div>
);

return disabled ? (
<Tooltip overlay={translate('hotspots.form.cannot_change_status')} placement="left">
{optionRender}
</Tooltip>
) : (
optionRender
);
}

+ 5
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx Dosyayı Görüntüle

@@ -21,6 +21,7 @@ import * as classNames from 'classnames';
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { RawHotspot } from '../../../types/security-hotspots';
import { getStatusOptionFromStatusAndResolution } from '../utils';

export interface HotspotListItemProps {
hotspot: RawHotspot;
@@ -37,7 +38,10 @@ export default function HotspotListItem(props: HotspotListItemProps) {
onClick={() => !selected && props.onClick(hotspot.key)}>
<div className="little-spacer-left">{hotspot.message}</div>
<div className="badge spacer-top">
{translate('hotspot.status', hotspot.resolution || hotspot.status)}
{translate(
'hotspots.status_option',
getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution)
)}
</div>
</a>
);

+ 5
- 16
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx Dosyayı Görüntüle

@@ -20,26 +20,23 @@
import * as React from 'react';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { isLoggedIn } from '../../../helpers/users';
import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import Assignee from './assignee/Assignee';
import HotspotActions from './HotspotActions';
import HotspotSnippetContainer from './HotspotSnippetContainer';
import HotspotViewerTabs from './HotspotViewerTabs';
import Status from './status/Status';

export interface HotspotViewerRendererProps {
branchLike?: BranchLike;
currentUser: T.CurrentUser;
hotspot?: Hotspot;
loading: boolean;
onUpdateHotspot: () => void;
securityCategories: T.StandardSecurityCategories;
}

export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
const { branchLike, currentUser, hotspot, loading, securityCategories } = props;
export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
const { branchLike, hotspot, loading, securityCategories } = props;

return (
<DeferredSpinner loading={loading}>
@@ -48,9 +45,6 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
<div className="big-spacer-bottom">
<div className="display-flex-space-between">
<h1>{hotspot.message}</h1>
{isLoggedIn(currentUser) && (
<HotspotActions hotspot={hotspot} onSubmit={props.onUpdateHotspot} />
)}
</div>
<div className="text-muted">
<span>{translate('category')}:</span>
@@ -59,12 +53,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
</span>
</div>
</div>
<div className="huge-spacer-bottom">
<span>{translate('status')}:</span>
<span className="badge little-spacer-left">
{translate('hotspot.status', hotspot.resolution || hotspot.status)}
</span>
<div className="display-flex-row huge-spacer-bottom">
<Assignee hotspot={hotspot} onAssigneeChange={props.onUpdateHotspot} />
<Status hotspot={hotspot} onStatusChange={props.onUpdateHotspot} />
</div>
<HotspotSnippetContainer branchLike={branchLike} hotspot={hotspot} />
<HotspotViewerTabs hotspot={hotspot} onUpdateHotspot={props.onUpdateHotspot} />
@@ -73,5 +64,3 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
</DeferredSpinner>
);
}

export default withCurrentUser(HotspotViewerRenderer);

+ 0
- 78
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx Dosyayı Görüntüle

@@ -1,78 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { Button } from 'sonar-ui-common/components/controls/buttons';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { HotspotStatus } from '../../../../types/security-hotspots';
import HotspotActions, { HotspotActionsProps } from '../HotspotActions';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should open when clicked', async () => {
const wrapper = shallowRender();

wrapper.find(Button).simulate('click');

await waitAndUpdate(wrapper);

expect(wrapper).toMatchSnapshot();
});

it('should register an eventlistener', () => {
let useEffectCleanup: void | (() => void | undefined) = () =>
fail('useEffect should clean after itself');
jest.spyOn(React, 'useEffect').mockImplementationOnce(f => {
useEffectCleanup = f() || useEffectCleanup;
});
let listenerCallback = (_event: { key: string }) =>
fail('Effect should have registered callback');
const addEventListener = jest.fn((_event, callback) => {
listenerCallback = callback;
});
jest.spyOn(document, 'addEventListener').mockImplementation(addEventListener);
const removeEventListener = jest.spyOn(document, 'removeEventListener');
const wrapper = shallowRender();

wrapper.find(Button).simulate('click');
expect(wrapper).toMatchSnapshot('Dropdown open');

listenerCallback({ key: 'whatever' });
expect(wrapper).toMatchSnapshot('Dropdown still open');

listenerCallback({ key: 'Escape' });
expect(wrapper).toMatchSnapshot('Dropdown closed');

useEffectCleanup();
expect(removeEventListener).toBeCalledWith('keydown', listenerCallback, false);
});

function shallowRender(props: Partial<HotspotActionsProps> = {}) {
return shallow(
<HotspotActions
hotspot={mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })}
onSubmit={jest.fn()}
{...props}
/>
);
}

+ 0
- 155
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx Dosyayı Görüntüle

@@ -1,155 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
assignSecurityHotspot,
commentSecurityHotspot,
setSecurityHotspotStatus
} from '../../../../api/security-hotspots';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockLoggedInUser } from '../../../../helpers/testMocks';
import {
HotspotResolution,
HotspotStatus,
HotspotStatusOption
} from '../../../../types/security-hotspots';
import HotspotActionsForm from '../HotspotActionsForm';

jest.mock('../../../../api/security-hotspots', () => ({
assignSecurityHotspot: jest.fn().mockResolvedValue(undefined),
commentSecurityHotspot: jest.fn().mockResolvedValue(undefined),
setSecurityHotspotStatus: jest.fn().mockResolvedValue(undefined)
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should handle option selection', () => {
const wrapper = shallowRender();
expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.FIXED);
wrapper.instance().handleSelectOption(HotspotStatusOption.SAFE);
expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.SAFE);
});

it('should handle comment change', () => {
const wrapper = shallowRender();
wrapper.instance().handleCommentChange('new comment');
expect(wrapper.state().comment).toBe('new comment');
});

describe('submit', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should be handled for additional review', async () => {
const onSubmit = jest.fn();
const wrapper = shallowRender({ onSubmit });
wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW });

const promise = wrapper.instance().handleSubmit(mockEvent());

expect(wrapper.state().submitting).toBe(true);
await promise;
expect(setSecurityHotspotStatus).toBeCalledWith('key', {
status: HotspotStatus.TO_REVIEW
});
expect(onSubmit).toBeCalled();
});

it('should be handled for SAFE', async () => {
const wrapper = shallowRender();
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOption.SAFE });
await wrapper.instance().handleSubmit(mockEvent());
expect(setSecurityHotspotStatus).toBeCalledWith('key', {
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
});
expect(commentSecurityHotspot).toBeCalledWith('key', 'commentsafe');
});

it('should be handled for FIXED', async () => {
const wrapper = shallowRender({
hotspot: mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })
});
wrapper.setState({ comment: 'commentfixed', selectedOption: HotspotStatusOption.FIXED });
await wrapper.instance().handleSubmit(mockEvent());
expect(setSecurityHotspotStatus).toBeCalledWith('key', {
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.FIXED
});
expect(commentSecurityHotspot).toBeCalledWith('key', 'commentfixed');
});

it('should ignore no change', async () => {
const wrapper = shallowRender();
wrapper.setState({ selectedOption: HotspotStatusOption.FIXED });
await wrapper.instance().handleSubmit(mockEvent());
expect(setSecurityHotspotStatus).not.toBeCalled();
});
});

it('should handle assignment', async () => {
const onSubmit = jest.fn();
const wrapper = shallowRender({ onSubmit });
wrapper.setState({
comment: 'assignment comment',
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW
});

wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' }));
await waitAndUpdate(wrapper);

const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);

expect(wrapper.state().submitting).toBe(true);
await promise;

expect(setSecurityHotspotStatus).toBeCalledWith('key', {
status: HotspotStatus.TO_REVIEW
});
expect(assignSecurityHotspot).toBeCalledWith('key', {
assignee: 'userLogin'
});
expect(commentSecurityHotspot).toBeCalledWith('key', 'assignment comment');
expect(onSubmit).toBeCalled();
});

it('should handle submit failure', async () => {
const onSubmit = jest.fn();
(setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure');
const wrapper = shallowRender({ onSubmit });
wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW });
const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);
expect(wrapper.state().submitting).toBe(true);
await promise;
await waitAndUpdate(wrapper);
expect(wrapper.state().submitting).toBe(false);
expect(onSubmit).not.toBeCalled();
});

function shallowRender(props: Partial<HotspotActionsForm['props']> = {}) {
return shallow<HotspotActionsForm>(
<HotspotActionsForm hotspot={mockHotspot({ key: 'key' })} onSubmit={jest.fn()} {...props} />
);
}

+ 0
- 109
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx Dosyayı Görüntüle

@@ -1,109 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockLoggedInUser } from '../../../../helpers/testMocks';
import {
HotspotResolution,
HotspotStatus,
HotspotStatusOption
} from '../../../../types/security-hotspots';
import HotspotActionsForm from '../HotspotActionsForm';
import HotspotActionsFormRenderer, {
HotspotActionsFormRendererProps
} from '../HotspotActionsFormRenderer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting');
expect(shallowRender({ selectedOption: HotspotStatusOption.SAFE })).toMatchSnapshot(
'safe option selected'
);
expect(
shallowRender({
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW,
selectedUser: mockLoggedInUser()
})
).toMatchSnapshot('user selected');
expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
'restricted access'
);
});

it('should enable the submit button if anything has changed', () => {
const hotspot = mockHotspot({
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
});
const selectedOption = HotspotStatusOption.SAFE;
expect(
shallowRender({ comment: '', hotspot, selectedOption, selectedUser: undefined })
.find(SubmitButton)
.props().disabled
).toBe(true);
expect(
shallowRender({ comment: 'some comment', hotspot, selectedOption, selectedUser: undefined })
.find(SubmitButton)
.props().disabled
).toBe(false);
expect(
shallowRender({ comment: '', hotspot, selectedOption, selectedUser: mockLoggedInUser() })
.find(SubmitButton)
.props().disabled
).toBe(false);
expect(
shallowRender({
comment: '',
hotspot,
selectedOption: HotspotStatusOption.FIXED,
selectedUser: undefined
})
.find(SubmitButton)
.props().disabled
).toBe(false);
expect(
shallowRender({
comment: '',
hotspot,
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW,
selectedUser: undefined
})
.find(SubmitButton)
.props().disabled
).toBe(false);
});

function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) {
return shallow<HotspotActionsForm>(
<HotspotActionsFormRenderer
comment="written comment"
hotspot={mockHotspot({ key: 'key' })}
onAssign={jest.fn()}
onChangeComment={jest.fn()}
onSelectOption={jest.fn()}
onSubmit={jest.fn()}
selectedOption={HotspotStatusOption.FIXED}
submitting={false}
{...props}
/>
);
}

+ 2
- 4
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx Dosyayı Görüntüle

@@ -20,8 +20,8 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks';
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';
import { mockUser } from '../../../../helpers/testMocks';
import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';

it('should render correctly', () => {
const wrapper = shallowRender();
@@ -41,13 +41,11 @@ it('should render correctly', () => {
})
).toMatchSnapshot('assignee without name');
expect(shallowRender()).toMatchSnapshot('anonymous user');
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in');
});

function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
currentUser={mockCurrentUser()}
hotspot={mockHotspot()}
loading={false}
onUpdateHotspot={jest.fn()}

+ 0
- 406
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap Dosyayı Görüntüle

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

exports[`should open when clicked 1`] = `
<div
className="dropdown big-spacer-left flex-0"
>
<Button
onClick={[Function]}
>
hotspot.change_status.TO_REVIEW
<DropdownIcon
className="little-spacer-left"
/>
</Button>
<OutsideClickHandler
onClickOutside={[Function]}
>
<DropdownOverlay
placement="bottom-right"
>
<HotspotActionsForm
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 {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "key",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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": "TO_REVIEW",
"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",
},
],
}
}
onSubmit={[Function]}
/>
</DropdownOverlay>
</OutsideClickHandler>
</div>
`;

exports[`should register an eventlistener: Dropdown closed 1`] = `
<div
className="dropdown big-spacer-left flex-0"
>
<Button
onClick={[Function]}
>
hotspot.change_status.TO_REVIEW
<DropdownIcon
className="little-spacer-left"
/>
</Button>
</div>
`;

exports[`should register an eventlistener: Dropdown open 1`] = `
<div
className="dropdown big-spacer-left flex-0"
>
<Button
onClick={[Function]}
>
hotspot.change_status.TO_REVIEW
<DropdownIcon
className="little-spacer-left"
/>
</Button>
<OutsideClickHandler
onClickOutside={[Function]}
>
<DropdownOverlay
placement="bottom-right"
>
<HotspotActionsForm
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 {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "key",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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": "TO_REVIEW",
"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",
},
],
}
}
onSubmit={[Function]}
/>
</DropdownOverlay>
</OutsideClickHandler>
</div>
`;

exports[`should register an eventlistener: Dropdown still open 1`] = `
<div
className="dropdown big-spacer-left flex-0"
>
<Button
onClick={[Function]}
>
hotspot.change_status.TO_REVIEW
<DropdownIcon
className="little-spacer-left"
/>
</Button>
<OutsideClickHandler
onClickOutside={[Function]}
>
<DropdownOverlay
placement="bottom-right"
>
<HotspotActionsForm
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 {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "key",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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": "TO_REVIEW",
"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",
},
],
}
}
onSubmit={[Function]}
/>
</DropdownOverlay>
</OutsideClickHandler>
</div>
`;

exports[`should render correctly 1`] = `
<div
className="dropdown big-spacer-left flex-0"
>
<Button
onClick={[Function]}
>
hotspot.change_status.TO_REVIEW
<DropdownIcon
className="little-spacer-left"
/>
</Button>
</div>
`;

+ 0
- 112
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly 1`] = `
<HotspotActionsFormRenderer
comment=""
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 {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "key",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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",
},
],
}
}
onAssign={[Function]}
onChangeComment={[Function]}
onSelectOption={[Function]}
onSubmit={[Function]}
selectedOption="FIXED"
submitting={false}
/>
`;

+ 0
- 544
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly 1`] = `
<form
className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={true}
className=""
onCheck={[MockFunction]}
value="FIXED"
>
<h3
className=""
>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="SAFE"
>
<h3
className=""
>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3
className=""
>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="display-flex-column big-spacer-bottom"
>
<label
className="little-spacer-bottom"
>
hotspots.form.comment
</label>
<textarea
autoFocus={true}
className="form-field fixed-width spacer-bottom"
onChange={[Function]}
placeholder=""
rows={6}
value="written comment"
/>
<MarkdownTips />
</div>
<div
className="text-right"
>
<SubmitButton
disabled={false}
>
hotspots.form.submit.REVIEWED
</SubmitButton>
</div>
</form>
`;

exports[`should render correctly: Submitting 1`] = `
<form
className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={true}
className=""
onCheck={[MockFunction]}
value="FIXED"
>
<h3
className=""
>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="SAFE"
>
<h3
className=""
>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3
className=""
>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="display-flex-column big-spacer-bottom"
>
<label
className="little-spacer-bottom"
>
hotspots.form.comment
</label>
<textarea
autoFocus={true}
className="form-field fixed-width spacer-bottom"
onChange={[Function]}
placeholder=""
rows={6}
value="written comment"
/>
<MarkdownTips />
</div>
<div
className="text-right"
>
<i
className="spinner spacer-right"
/>
<SubmitButton
disabled={true}
>
hotspots.form.submit.REVIEWED
</SubmitButton>
</div>
</form>
`;

exports[`should render correctly: restricted access 1`] = `
<form
className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title.disabled
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<Tooltip
overlay="hotspots.form.cannot_change_status"
placement="left"
>
<div
className="big-spacer-top"
>
<Radio
checked={true}
className="disabled"
onCheck={[Function]}
value="FIXED"
>
<h3
className="text-muted"
>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description text-muted"
>
hotspots.status_option.FIXED.description
</div>
</div>
</Tooltip>
<Tooltip
overlay="hotspots.form.cannot_change_status"
placement="left"
>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className="disabled"
onCheck={[Function]}
value="SAFE"
>
<h3
className="text-muted"
>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description text-muted"
>
hotspots.status_option.SAFE.description
</div>
</div>
</Tooltip>
<Tooltip
overlay="hotspots.form.cannot_change_status"
placement="left"
>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className="disabled"
onCheck={[Function]}
value="ADDITIONAL_REVIEW"
>
<h3
className="text-muted"
>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description text-muted"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</Tooltip>
</div>
<div
className="display-flex-column big-spacer-bottom"
>
<label
className="little-spacer-bottom"
>
hotspots.form.comment
</label>
<textarea
autoFocus={true}
className="form-field fixed-width spacer-bottom"
onChange={[Function]}
placeholder=""
rows={6}
value="written comment"
/>
<MarkdownTips />
</div>
<div
className="text-right"
>
<SubmitButton
disabled={false}
>
hotspots.form.submit.REVIEWED
</SubmitButton>
</div>
</form>
`;

exports[`should render correctly: safe option selected 1`] = `
<form
className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="FIXED"
>
<h3
className=""
>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={true}
className=""
onCheck={[MockFunction]}
value="SAFE"
>
<h3
className=""
>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3
className=""
>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="display-flex-column big-spacer-bottom"
>
<label
className="little-spacer-bottom"
>
hotspots.form.comment
</label>
<textarea
autoFocus={true}
className="form-field fixed-width spacer-bottom"
onChange={[Function]}
placeholder="hotspots.form.comment.placeholder"
rows={6}
value="written comment"
/>
<MarkdownTips />
</div>
<div
className="text-right"
>
<SubmitButton
disabled={false}
>
hotspots.form.submit.REVIEWED
</SubmitButton>
</div>
</form>
`;

exports[`should render correctly: user selected 1`] = `
<form
className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="FIXED"
>
<h3
className=""
>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
className=""
onCheck={[MockFunction]}
value="SAFE"
>
<h3
className=""
>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={true}
className=""
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3
className=""
>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="display-flex-column big-spacer-bottom"
>
<label
className="little-spacer-bottom"
>
hotspots.form.comment
</label>
<textarea
autoFocus={true}
className="form-field fixed-width spacer-bottom"
onChange={[Function]}
placeholder=""
rows={6}
value="written comment"
/>
<MarkdownTips />
</div>
<div
className="text-right"
>
<SubmitButton
disabled={false}
>
hotspots.form.submit.REVIEWED
</SubmitButton>
</div>
</form>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap Dosyayı Görüntüle

@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = `
<div
className="badge spacer-top"
>
hotspot.status.TO_REVIEW
hotspots.status_option.TO_REVIEW
</div>
</a>
`;
@@ -33,7 +33,7 @@ exports[`should render correctly 2`] = `
<div
className="badge spacer-top"
>
hotspot.status.TO_REVIEW
hotspots.status_option.TO_REVIEW
</div>
</a>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly 1`] = `
<Connect(withCurrentUser(HotspotViewerRenderer))
<HotspotViewerRenderer
loading={true}
onUpdateHotspot={[Function]}
securityCategories={
@@ -15,7 +15,7 @@ exports[`should render correctly 1`] = `
`;

exports[`should render correctly 2`] = `
<Connect(withCurrentUser(HotspotViewerRenderer))
<HotspotViewerRenderer
hotspot={
Object {
"id": "I am a detailled hotspot",

+ 450
- 441
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
Dosya farkı çok büyük olduğundan ihmal edildi
Dosyayı Görüntüle


+ 1
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx Dosyayı Görüntüle

@@ -47,7 +47,7 @@ export default function AssigneeSelectionRenderer(props: HotspotAssigneeSelectRe
autoFocus={true}
onChange={props.onSearch}
onKeyDown={props.onKeyDown}
placeholder={translate('hotspots.form.select_user')}
placeholder={translate('hotspots.assignee.select_user')}
value={query}
/>


+ 4
- 4
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap Dosyayı Görüntüle

@@ -9,7 +9,7 @@ exports[`should render correctly 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
placeholder="hotspots.form.select_user"
placeholder="hotspots.assignee.select_user"
/>
</div>
</Fragment>
@@ -24,7 +24,7 @@ exports[`should render correctly: loading 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
placeholder="hotspots.form.select_user"
placeholder="hotspots.assignee.select_user"
/>
<DeferredSpinner
className="spacer-left"
@@ -43,7 +43,7 @@ exports[`should render correctly: open 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
placeholder="hotspots.form.select_user"
placeholder="hotspots.assignee.select_user"
/>
</div>
<div
@@ -73,7 +73,7 @@ exports[`should render correctly: open with results 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
placeholder="hotspots.form.select_user"
placeholder="hotspots.assignee.select_user"
/>
</div>
<div

+ 44
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css Dosyayı Görüntüle

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/

#status-trigger,
.popup {
width: 400px;
box-sizing: border-box;
}

#status-trigger {
height: 80px;
border-radius: 4px;
outline: none;
}

#status-trigger.readonly {
cursor: not-allowed;
}

#status-trigger:not(.readonly) {
cursor: pointer;
background-color: var(--darkBlue);
}

#status-trigger:not(.readonly) * {
color: white;
}

+ 107
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx Dosyayı Görüntüle

@@ -0,0 +1,107 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import Toggler from 'sonar-ui-common/components/controls/Toggler';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import ChevronDownIcon from 'sonar-ui-common/components/icons/ChevronDownIcon';
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { withCurrentUser } from '../../../../components/hoc/withCurrentUser';
import { isLoggedIn } from '../../../../helpers/users';
import { Hotspot } from '../../../../types/security-hotspots';
import { getStatusOptionFromStatusAndResolution } from '../../utils';
import './Status.css';
import StatusDescription from './StatusDescription';
import StatusSelection from './StatusSelection';

export interface StatusProps {
currentUser: T.CurrentUser;
hotspot: Hotspot;

onStatusChange: () => void;
}

export function Status(props: StatusProps) {
const { currentUser, hotspot } = props;
const [isOpen, setIsOpen] = React.useState(false);

const statusOption = getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution);
const readonly = !hotspot.canChangeStatus || !isLoggedIn(currentUser);

const trigger = (
<div
aria-expanded={isOpen}
aria-haspopup={true}
className={classNames('padded bordered display-flex-column display-flex-justify-center', {
readonly
})}
id="status-trigger"
onClick={() => !readonly && setIsOpen(true)}
role="button"
tabIndex={0}>
<div className="display-flex-center display-flex-space-between">
{isOpen ? (
<span className="h3">{translate('hotspots.status.select_status')}</span>
) : (
<StatusDescription showTitle={true} statusOption={statusOption} />
)}
{!readonly && <ChevronDownIcon className="big-spacer-left" />}
</div>
</div>
);

const actionableTrigger = (
<Toggler
closeOnClickOutside={true}
closeOnEscape={true}
onRequestClose={() => setIsOpen(false)}
open={isOpen}
overlay={
<DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}>
<StatusSelection
hotspot={hotspot}
onStatusOptionChange={() => {
setIsOpen(false);
props.onStatusChange();
}}
/>
</DropdownOverlay>
}>
{trigger}
</Toggler>
);

return (
<div className="dropdown huge-spacer-left">
{readonly ? (
<Tooltip overlay={translate('hotspots.status.cannot_change_status')} placement="bottom">
{actionableTrigger}
</Tooltip>
) : (
actionableTrigger
)}
</div>
);
}

export default withCurrentUser(Status);

+ 41
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx Dosyayı Görüntüle

@@ -0,0 +1,41 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { HotspotStatusOption } from '../../../../types/security-hotspots';

export interface StatusDescriptionProps {
statusOption: HotspotStatusOption;
showTitle?: boolean;
}

export default function StatusDescription(props: StatusDescriptionProps) {
const { statusOption, showTitle } = props;

return (
<div>
<h3>
{showTitle && `${translate('status')}: `}
{translate('hotspots.status_option', statusOption)}
</h3>
<span>{translate('hotspots.status_option', statusOption, 'description')}</span>
</div>
);
}

+ 109
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx Dosyayı Görüntüle

@@ -0,0 +1,109 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 React from 'react';
import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
import {
getStatusAndResolutionFromStatusOption,
getStatusOptionFromStatusAndResolution
} from '../../utils';
import StatusSelectionRenderer from './StatusSelectionRenderer';

interface Props {
hotspot: Hotspot;
onStatusOptionChange: (statusOption: HotspotStatusOption) => void;
}

interface State {
comment?: string;
loading: boolean;
initialStatus: HotspotStatusOption;
selectedStatus: HotspotStatusOption;
}

export default class StatusSelection extends React.PureComponent<Props, State> {
mounted = false;

constructor(props: Props) {
super(props);

const initialStatus = getStatusOptionFromStatusAndResolution(
props.hotspot.status,
props.hotspot.resolution
);

this.state = {
loading: false,
initialStatus,
selectedStatus: initialStatus
};
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleStatusChange = (selectedStatus: HotspotStatusOption) => {
this.setState({ selectedStatus });
};

handleCommentChange = (comment: string) => {
this.setState({ comment });
};

handleSubmit = () => {
const { hotspot } = this.props;
const { comment, initialStatus, selectedStatus } = this.state;

if (selectedStatus && selectedStatus !== initialStatus) {
this.setState({ loading: true });
setSecurityHotspotStatus(hotspot.key, {
...getStatusAndResolutionFromStatusOption(selectedStatus),
comment: comment || undefined
})
.then(() => {
this.setState({ loading: false });
this.props.onStatusOptionChange(selectedStatus);
})
.catch(() => this.setState({ loading: false }));
}
};

render() {
const { comment, initialStatus, loading, selectedStatus } = this.state;
const submitDisabled = selectedStatus === initialStatus;

return (
<StatusSelectionRenderer
comment={comment}
loading={loading}
onCommentChange={this.handleCommentChange}
onStatusChange={this.handleStatusChange}
onSubmit={this.handleSubmit}
selectedStatus={selectedStatus}
submitDisabled={submitDisabled}
/>
);
}
}

+ 91
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx Dosyayı Görüntüle

@@ -0,0 +1,91 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 React from 'react';
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Radio from 'sonar-ui-common/components/controls/Radio';
import { translate } from 'sonar-ui-common/helpers/l10n';
import MarkdownTips from '../../../../components/common/MarkdownTips';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import StatusDescription from './StatusDescription';

export interface StatusSelectionRendererProps {
selectedStatus: HotspotStatusOption;
onStatusChange: (statusOption: HotspotStatusOption) => void;

comment?: string;
onCommentChange: (comment: string) => void;

onSubmit: () => void;

loading: boolean;
submitDisabled: boolean;
}

export default function StatusSelectionRenderer(props: StatusSelectionRendererProps) {
const { comment, loading, selectedStatus, submitDisabled } = props;

const renderOption = (status: HotspotStatusOption) => {
return (
<Radio
checked={selectedStatus === status}
className="big-spacer-bottom"
onCheck={props.onStatusChange}
value={status}>
<StatusDescription statusOption={status} />
</Radio>
);
};

return (
<>
<div className="big-padded">
{renderOption(HotspotStatusOption.TO_REVIEW)}
{renderOption(HotspotStatusOption.FIXED)}
{renderOption(HotspotStatusOption.SAFE)}
</div>

<hr />
<div className="big-padded display-flex-column">
<label className="text-bold" htmlFor="comment-textarea">
{translate('hotspots.status.add_comment')}
</label>
<textarea
className="spacer-top form-field fixed-width spacer-bottom"
id="comment-textarea"
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
props.onCommentChange(event.currentTarget.value)
}
rows={4}
value={comment}
/>
<MarkdownTips />

<div className="big-spacer-top display-flex-justify-end display-flex-center">
<SubmitButton disabled={submitDisabled || loading} onClick={props.onSubmit}>
{translate('hotspots.status.change_status')}
</SubmitButton>

{loading && <i className="spacer-left spinner" />}
</div>
</div>
</>
);
}

+ 74
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx Dosyayı Görüntüle

@@ -0,0 +1,74 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import Toggler from 'sonar-ui-common/components/controls/Toggler';
import { click } from 'sonar-ui-common/helpers/testUtils';
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser } from '../../../../../helpers/testMocks';
import { HotspotStatusOption } from '../../../../../types/security-hotspots';
import { Status, StatusProps } from '../Status';
import StatusSelection from '../StatusSelection';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot('closed');

click(wrapper.find('#status-trigger'));
expect(wrapper).toMatchSnapshot('open');

wrapper
.find(Toggler)
.props()
.onRequestClose();
expect(wrapper.find(DropdownOverlay).length).toBe(0);

expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
'readonly'
);
});

it('should properly deal with status changes', () => {
const onStatusChange = jest.fn();
const wrapper = shallowRender({ onStatusChange });

click(wrapper.find('#status-trigger'));
wrapper
.find(Toggler)
.dive()
.find(StatusSelection)
.props()
.onStatusOptionChange(HotspotStatusOption.SAFE);
expect(onStatusChange).toHaveBeenCalled();
expect(wrapper.find(DropdownOverlay).length).toBe(0);
});

function shallowRender(props?: Partial<StatusProps>) {
return shallow<StatusProps>(
<Status
currentUser={mockCurrentUser({ isLoggedIn: true })}
hotspot={mockHotspot()}
onStatusChange={jest.fn()}
{...props}
/>
);
}

+ 35
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx Dosyayı Görüntüle

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 StatusDescription, { StatusDescriptionProps } from '../StatusDescription';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ showTitle: true })).toMatchSnapshot('with title');
});

function shallowRender(props?: Partial<StatusDescriptionProps>) {
return shallow<StatusDescriptionProps>(
<StatusDescription statusOption={HotspotStatusOption.TO_REVIEW} {...props} />
);
}

+ 78
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx Dosyayı Görüntüle

@@ -0,0 +1,78 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { setSecurityHotspotStatus } from '../../../../../api/security-hotspots';
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
import { HotspotStatus, HotspotStatusOption } from '../../../../../types/security-hotspots';
import StatusSelection from '../StatusSelection';
import StatusSelectionRenderer from '../StatusSelectionRenderer';

jest.mock('../../../../../api/security-hotspots', () => ({
setSecurityHotspotStatus: jest.fn()
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should properly deal with comment/status/submit events', async () => {
const hotspot = mockHotspot();
const onStatusOptionChange = jest.fn();
const wrapper = shallowRender({ hotspot, onStatusOptionChange });

const newStatusOption = HotspotStatusOption.SAFE;
wrapper
.find(StatusSelectionRenderer)
.props()
.onStatusChange(newStatusOption);
expect(wrapper.state().selectedStatus).toBe(newStatusOption);
expect(wrapper.find(StatusSelectionRenderer).props().submitDisabled).toBe(false);

const newComment = 'TEST-COMMENT';
wrapper
.find(StatusSelectionRenderer)
.props()
.onCommentChange(newComment);
expect(wrapper.state().comment).toBe(newComment);

(setSecurityHotspotStatus as jest.Mock).mockResolvedValueOnce({});
wrapper
.find(StatusSelectionRenderer)
.props()
.onSubmit();
expect(setSecurityHotspotStatus).toHaveBeenCalledWith(hotspot.key, {
status: HotspotStatus.REVIEWED,
resolution: HotspotStatusOption.SAFE,
comment: newComment
});

await waitAndUpdate(wrapper);

expect(onStatusOptionChange).toHaveBeenCalledWith(newStatusOption);
});

function shallowRender(props?: Partial<StatusSelection['props']>) {
return shallow<StatusSelection>(
<StatusSelection hotspot={mockHotspot()} onStatusOptionChange={jest.fn()} {...props} />
);
}

+ 72
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx Dosyayı Görüntüle

@@ -0,0 +1,72 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Radio from 'sonar-ui-common/components/controls/Radio';
import { change, click } from 'sonar-ui-common/helpers/testUtils';
import { HotspotStatusOption } from '../../../../../types/security-hotspots';
import StatusSelectionRenderer, { StatusSelectionRendererProps } from '../StatusSelectionRenderer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
expect(
shallowRender({ submitDisabled: true })
.find(SubmitButton)
.props().disabled
).toBe(true);
});

it('should call proper callbacks on actions', () => {
const onCommentChange = jest.fn();
const onStatusChange = jest.fn();
const onSubmit = jest.fn();
const wrapper = shallowRender({ onCommentChange, onStatusChange, onSubmit });

change(wrapper.find('textarea'), 'TATA');
expect(onCommentChange).toHaveBeenCalledWith('TATA');

wrapper
.find(Radio)
.first()
.props()
.onCheck(HotspotStatusOption.SAFE);
expect(onStatusChange).toHaveBeenCalledWith(HotspotStatusOption.SAFE);

click(wrapper.find(SubmitButton));
expect(onSubmit).toHaveBeenCalled();
});

function shallowRender(props?: Partial<StatusSelectionRendererProps>) {
return shallow<StatusSelectionRendererProps>(
<StatusSelectionRenderer
comment="TEST-COMMENT"
loading={false}
onCommentChange={jest.fn()}
onStatusChange={jest.fn()}
onSubmit={jest.fn()}
selectedStatus={HotspotStatusOption.TO_REVIEW}
submitDisabled={false}
{...props}
/>
);
}

+ 436
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly: closed 1`] = `
<div
className="dropdown huge-spacer-left"
>
<Toggler
closeOnClickOutside={true}
closeOnEscape={true}
onRequestClose={[Function]}
open={false}
overlay={
<DropdownOverlay
noPadding={true}
placement="bottom"
>
<StatusSelection
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 {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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",
},
],
}
}
onStatusOptionChange={[Function]}
/>
</DropdownOverlay>
}
>
<div
aria-expanded={false}
aria-haspopup={true}
className="padded bordered display-flex-column display-flex-justify-center"
id="status-trigger"
onClick={[Function]}
role="button"
tabIndex={0}
>
<div
className="display-flex-center display-flex-space-between"
>
<StatusDescription
showTitle={true}
statusOption="FIXED"
/>
<ChevronDownIcon
className="big-spacer-left"
/>
</div>
</div>
</Toggler>
</div>
`;

exports[`should render correctly: open 1`] = `
<div
className="dropdown huge-spacer-left"
>
<Toggler
closeOnClickOutside={true}
closeOnEscape={true}
onRequestClose={[Function]}
open={true}
overlay={
<DropdownOverlay
noPadding={true}
placement="bottom"
>
<StatusSelection
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 {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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",
},
],
}
}
onStatusOptionChange={[Function]}
/>
</DropdownOverlay>
}
>
<div
aria-expanded={true}
aria-haspopup={true}
className="padded bordered display-flex-column display-flex-justify-center"
id="status-trigger"
onClick={[Function]}
role="button"
tabIndex={0}
>
<div
className="display-flex-center display-flex-space-between"
>
<span
className="h3"
>
hotspots.status.select_status
</span>
<ChevronDownIcon
className="big-spacer-left"
/>
</div>
</div>
</Toggler>
</div>
`;

exports[`should render correctly: readonly 1`] = `
<div
className="dropdown huge-spacer-left"
>
<Tooltip
overlay="hotspots.status.cannot_change_status"
placement="bottom"
>
<Toggler
closeOnClickOutside={true}
closeOnEscape={true}
onRequestClose={[Function]}
open={false}
overlay={
<DropdownOverlay
noPadding={true}
placement="bottom"
>
<StatusSelection
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": false,
"changelog": Array [],
"comment": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"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 [],
},
"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",
},
],
}
}
onStatusOptionChange={[Function]}
/>
</DropdownOverlay>
}
>
<div
aria-expanded={false}
aria-haspopup={true}
className="padded bordered display-flex-column display-flex-justify-center readonly"
id="status-trigger"
onClick={[Function]}
role="button"
tabIndex={0}
>
<div
className="display-flex-center display-flex-space-between"
>
<StatusDescription
showTitle={true}
statusOption="FIXED"
/>
</div>
</div>
</Toggler>
</Tooltip>
</div>
`;

+ 24
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly 1`] = `
<div>
<h3>
hotspots.status_option.TO_REVIEW
</h3>
<span>
hotspots.status_option.TO_REVIEW.description
</span>
</div>
`;

exports[`should render correctly: with title 1`] = `
<div>
<h3>
status:
hotspots.status_option.TO_REVIEW
</h3>
<span>
hotspots.status_option.TO_REVIEW.description
</span>
</div>
`;

+ 12
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly 1`] = `
<StatusSelectionRenderer
loading={false}
onCommentChange={[Function]}
onStatusChange={[Function]}
onSubmit={[Function]}
selectedStatus="FIXED"
submitDisabled={true}
/>
`;

+ 140
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap Dosyayı Görüntüle

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

exports[`should render correctly 1`] = `
<Fragment>
<div
className="big-padded"
>
<Radio
checked={true}
className="big-spacer-bottom"
onCheck={[MockFunction]}
value="TO_REVIEW"
>
<StatusDescription
statusOption="TO_REVIEW"
/>
</Radio>
<Radio
checked={false}
className="big-spacer-bottom"
onCheck={[MockFunction]}
value="FIXED"
>
<StatusDescription
statusOption="FIXED"
/>
</Radio>
<Radio
checked={false}
className="big-spacer-bottom"
onCheck={[MockFunction]}
value="SAFE"
>
<StatusDescription
statusOption="SAFE"
/>
</Radio>
</div>
<hr />
<div
className="big-padded display-flex-column"
>
<label
className="text-bold"
htmlFor="comment-textarea"
>
hotspots.status.add_comment
</label>
<textarea
className="spacer-top form-field fixed-width spacer-bottom"
id="comment-textarea"
onChange={[Function]}
rows={4}
value="TEST-COMMENT"
/>
<MarkdownTips />
<div
className="big-spacer-top display-flex-justify-end display-flex-center"
>
<SubmitButton
disabled={false}
onClick={[MockFunction]}
>
hotspots.status.change_status
</SubmitButton>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: loading 1`] = `
<Fragment>
<div
className="big-padded"
>
<Radio
checked={true}
className="big-spacer-bottom"
onCheck={[MockFunction]}
value="TO_REVIEW"
>
<StatusDescription
statusOption="TO_REVIEW"
/>
</Radio>
<Radio
checked={false}
className="big-spacer-bottom"
onCheck={[MockFunction]}
value="FIXED"
>
<StatusDescription
statusOption="FIXED"
/>
</Radio>
<Radio
checked={false}
className="big-spacer-bottom"
onCheck={[MockFunction]}
value="SAFE"
>
<StatusDescription
statusOption="SAFE"
/>
</Radio>
</div>
<hr />
<div
className="big-padded display-flex-column"
>
<label
className="text-bold"
htmlFor="comment-textarea"
>
hotspots.status.add_comment
</label>
<textarea
className="spacer-top form-field fixed-width spacer-bottom"
id="comment-textarea"
onChange={[Function]}
rows={4}
value="TEST-COMMENT"
/>
<MarkdownTips />
<div
className="big-spacer-top display-flex-justify-end display-flex-center"
>
<SubmitButton
disabled={true}
onClick={[MockFunction]}
>
hotspots.status.change_status
</SubmitButton>
<i
className="spacer-left spinner"
/>
</div>
</div>
</Fragment>
`;

+ 0
- 7
server/sonar-web/src/main/js/apps/security-hotspots/styles.css Dosyayı Görüntüle

@@ -51,10 +51,3 @@
overflow-y: auto;
background-color: white;
}

/*
* Align description with label by offsetting by width of radio + margin
*/
#security_hotspots .radio-button-description {
margin-left: 23px;
}

+ 35
- 0
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts Dosyayı Görüntüle

@@ -20,6 +20,9 @@
import { groupBy, sortBy } from 'lodash';
import {
Hotspot,
HotspotResolution,
HotspotStatus,
HotspotStatusOption,
RawHotspot,
ReviewHistoryElement,
ReviewHistoryType,
@@ -137,3 +140,35 @@ export function getHotspotReviewHistory(
functionalCount
};
}

const STATUS_AND_RESOLUTION_TO_STATUS_OPTION = {
[HotspotStatus.TO_REVIEW]: HotspotStatusOption.TO_REVIEW,
[HotspotStatus.REVIEWED]: HotspotStatusOption.FIXED,
[HotspotResolution.FIXED]: HotspotStatusOption.FIXED,
[HotspotResolution.SAFE]: HotspotStatusOption.SAFE
};

export function getStatusOptionFromStatusAndResolution(
status: HotspotStatus,
resolution?: HotspotResolution
) {
// Resolution is the most determinist info here, so we use it first to get the matching status option
// If not provided, we use the status (which will be TO_REVIEW)
return STATUS_AND_RESOLUTION_TO_STATUS_OPTION[resolution ?? status];
}

const STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP = {
[HotspotStatusOption.TO_REVIEW]: { status: HotspotStatus.TO_REVIEW, resolution: undefined },
[HotspotStatusOption.FIXED]: {
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.FIXED
},
[HotspotStatusOption.SAFE]: {
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
}
};

export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) {
return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption];
}

+ 3
- 3
server/sonar-web/src/main/js/types/security-hotspots.ts Dosyayı Görüntüle

@@ -42,7 +42,7 @@ export enum HotspotStatusFilter {
export enum HotspotStatusOption {
FIXED = 'FIXED',
SAFE = 'SAFE',
ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW'
TO_REVIEW = 'TO_REVIEW'
}

export interface HotspotFilters {
@@ -60,10 +60,10 @@ export interface RawHotspot {
line?: number;
message: string;
project: string;
resolution?: string;
resolution?: HotspotResolution;
rule: string;
securityCategory: string;
status: string;
status: HotspotStatus;
subProject?: string;
updateDate: string;
vulnerabilityProbability: RiskExposure;

+ 11
- 27
sonar-core/src/main/resources/org/sonar/l10n/core.properties Dosyayı Görüntüle

@@ -664,9 +664,6 @@ hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed
hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe
hotspots.risk_exposure=Review priority:

hotspot.category=Category:
hotspot.status=Status:
hotspot.assigned_to=Assigned to:
hotspots.tabs.risk_description=What's the risk?
hotspots.tabs.vulnerability_description=Are you at risk?
hotspots.tabs.fix_recommendations=How can you fix it?
@@ -677,12 +674,17 @@ hotspots.tabs.review_history.comment.add=Add a comment
hotspots.tabs.review_history.comment.field=Comment:
hotspots.tabs.review_history.comment.submit=Comment

hotspot.change_status.REVIEWED=Change status
hotspot.change_status.TO_REVIEW=Review Hotspot

hotspot.status.TO_REVIEW=To review
hotspot.status.FIXED=Fixed
hotspot.status.SAFE=Safe
hotspots.assignee.select_user=Select a user...
hotspots.status.cannot_change_status=Changing a hotspot's status requires permission.
hotspots.status.select_status=Select a status...
hotspots.status.add_comment=Add a comment (Optional)
hotspots.status.change_status=Change status
hotspots.status_option.TO_REVIEW=To review
hotspots.status_option.TO_REVIEW.description=This Security Hotspot needs to be reviewed to assess whether the code poses a risk.
hotspots.status_option.FIXED=Fixed
hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices.
hotspots.status_option.SAFE=Safe
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.

hotspot.filters.title=Filters
hotspot.filters.assignee.assigned_to_me=Assigned to me
@@ -697,24 +699,6 @@ hotspot.filters.show_all=Show all hotspots
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot

hotspots.form.title=Mark Security Hotspot as:
hotspots.form.title.disabled=Security Hotspot is marked as:

hotspots.form.cannot_change_status=Changing a hotspot's status requires permission.
hotspots.form.assign_to=Assign to:
hotspots.form.select_user=Select a user...
hotspots.form.comment=Comment:
hotspots.form.comment.placeholder=For tracking purposes, we highly recommend explaining why the code is safe.
hotspots.form.submit.TO_REVIEW=Submit Review
hotspots.form.submit.REVIEWED=Apply changes

hotspots.status_option.FIXED=Fixed
hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices.
hotspots.status_option.SAFE=Safe
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
hotspots.status_option.ADDITIONAL_REVIEW=Needs additional review
hotspots.status_option.ADDITIONAL_REVIEW.description=Someone else needs to review this Security Hotspot.

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

Loading…
İptal
Kaydet