* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON } from 'sonar-ui-common/helpers/request';
+import { getJSON, post } from 'sonar-ui-common/helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { DetailedHotspot, HotspotSearchResponse } from '../types/security-hotspots';
+import {
+ DetailedHotspot,
+ HotspotSearchResponse,
+ HotspotSetStatusRequest
+} from '../types/security-hotspots';
+
+export function setSecurityHotspotStatus(data: HotspotSetStatusRequest): Promise<void> {
+ return post('/api/hotspots/change_status', data).catch(throwGlobalError);
+}
export function getSecurityHotspots(data: {
projectKey: string;
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
const { hotspots, loading, securityCategories, selectedHotspotKey } = props;
+
return (
<div id="security_hotspots">
<FilterBar />
--- /dev/null
+/*
+ * 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 HotspotActionsForm from './HotspotActionsForm';
+
+export interface HotspotActionsProps {
+ hotspotKey: string;
+}
+
+const ESCAPE_KEY = 'Escape';
+
+export default function HotspotActions(props: HotspotActionsProps) {
+ 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">
+ <Button onClick={() => setOpen(!open)}>
+ {translate('hotspots.review_hotspot')}
+ <DropdownIcon className="little-spacer-left" />
+ </Button>
+
+ {open && (
+ <OutsideClickHandler onClickOutside={() => setOpen(false)}>
+ <DropdownOverlay placement={PopupPlacement.BottomRight}>
+ <HotspotActionsForm hotspotKey={props.hotspotKey} onSubmit={() => setOpen(false)} />
+ </DropdownOverlay>
+ </OutsideClickHandler>
+ )}
+ </div>
+ );
+}
--- /dev/null
+/*
+ * 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 {
+ HotspotResolution,
+ HotspotSetStatusRequest,
+ HotspotStatus,
+ HotspotStatusOptions
+} from '../../../types/security-hotspots';
+import HotspotActionsFormRenderer from './HotspotActionsFormRenderer';
+
+interface Props {
+ hotspotKey: string;
+ onSubmit: () => void;
+}
+
+interface State {
+ selectedOption: HotspotStatusOptions;
+ submitting: boolean;
+}
+
+export default class HotspotActionsForm extends React.Component<Props, State> {
+ state: State = {
+ selectedOption: HotspotStatusOptions.FIXED,
+ submitting: false
+ };
+
+ handleSelectOption = (selectedOption: HotspotStatusOptions) => {
+ this.setState({ selectedOption });
+ };
+
+ handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ const { hotspotKey } = this.props;
+ const { selectedOption } = this.state;
+
+ const status =
+ selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW
+ ? HotspotStatus.TO_REVIEW
+ : HotspotStatus.REVIEWED;
+ const data: HotspotSetStatusRequest = { hotspot: hotspotKey, status };
+ if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) {
+ data.resolution = HotspotResolution[selectedOption];
+ }
+
+ this.setState({ submitting: true });
+ return setSecurityHotspotStatus(data)
+ .then(() => {
+ this.props.onSubmit();
+ })
+ .finally(() => {
+ this.setState({ submitting: false });
+ });
+ };
+
+ render() {
+ const { hotspotKey } = this.props;
+ const { selectedOption, submitting } = this.state;
+
+ return (
+ <HotspotActionsFormRenderer
+ hotspotKey={hotspotKey}
+ onSelectOption={this.handleSelectOption}
+ onSubmit={this.handleSubmit}
+ selectedOption={selectedOption}
+ submitting={submitting}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 { HotspotStatusOptions } from '../../../types/security-hotspots';
+
+export interface HotspotActionsFormRendererProps {
+ hotspotKey: string;
+ onSelectOption: (option: HotspotStatusOptions) => void;
+ onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
+ selectedOption: HotspotStatusOptions;
+ submitting: boolean;
+}
+
+export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) {
+ const { selectedOption, submitting } = props;
+
+ return (
+ <form className="abs-width-400" onSubmit={props.onSubmit}>
+ <h2>{translate('hotspots.form.title')}</h2>
+ <div className="display-flex-column big-spacer-bottom">
+ {renderOption({
+ option: HotspotStatusOptions.FIXED,
+ selectedOption,
+ onClick: props.onSelectOption
+ })}
+ {renderOption({
+ option: HotspotStatusOptions.SAFE,
+ selectedOption,
+ onClick: props.onSelectOption
+ })}
+ {renderOption({
+ option: HotspotStatusOptions.ADDITIONAL_REVIEW,
+ selectedOption,
+ onClick: props.onSelectOption
+ })}
+ </div>
+ <div className="text-right">
+ {submitting && <i className="spinner spacer-right" />}
+ <SubmitButton disabled={submitting}>{translate('hotspots.form.submit')}</SubmitButton>
+ </div>
+ </form>
+ );
+}
+
+function renderOption(params: {
+ option: HotspotStatusOptions;
+ onClick: (option: HotspotStatusOptions) => void;
+ selectedOption: HotspotStatusOptions;
+}) {
+ const { onClick, option, selectedOption } = params;
+ return (
+ <div className="big-spacer-top">
+ <Radio checked={selectedOption === option} onCheck={onClick} value={option}>
+ <h3>{translate('hotspots.status_option', option)}</h3>
+ </Radio>
+ <div className="radio-button-description">
+ {translate('hotspots.status_option', option, 'description')}
+ </div>
+ </div>
+ );
+}
import * as React from 'react';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
+import { isLoggedIn } from '../../../helpers/users';
import { DetailedHotspot } from '../../../types/security-hotspots';
+import HotspotActions from './HotspotActions';
import HotspotViewerTabs from './HotspotViewerTabs';
export interface HotspotViewerRendererProps {
+ currentUser: T.CurrentUser;
hotspot?: DetailedHotspot;
loading: boolean;
securityCategories: T.StandardSecurityCategories;
}
-export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
- const { hotspot, loading, securityCategories } = props;
+export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
+ const { currentUser, hotspot, loading, securityCategories } = props;
return (
<DeferredSpinner loading={loading}>
{hotspot && (
<div className="big-padded">
<div className="big-spacer-bottom">
- <h1>{hotspot.message}</h1>
+ <div className="display-flex-space-between">
+ <h1>{hotspot.message}</h1>
+ {isLoggedIn(currentUser) && <HotspotActions hotspotKey={hotspot.key} />}
+ </div>
<div className="text-muted">
<span>{translate('hotspot.category')}</span>
<span className="little-spacer-left">
</DeferredSpinner>
);
}
+
+export default withCurrentUser(HotspotViewerRenderer);
--- /dev/null
+/*
+ * 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 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 hotspotKey="key" {...props} />);
+}
--- /dev/null
+/*
+ * 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 {
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusOptions
+} from '../../../../types/security-hotspots';
+import HotspotActionsForm from '../HotspotActionsForm';
+
+jest.mock('../../../../api/security-hotspots', () => ({
+ 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(HotspotStatusOptions.FIXED);
+ wrapper.instance().handleSelectOption(HotspotStatusOptions.SAFE);
+ expect(wrapper.state().selectedOption).toBe(HotspotStatusOptions.SAFE);
+});
+
+it('should handle submit', async () => {
+ const onSubmit = jest.fn();
+ const wrapper = shallowRender({ onSubmit });
+ wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW });
+ await waitAndUpdate(wrapper);
+
+ const preventDefault = jest.fn();
+ const promise = wrapper.instance().handleSubmit({ preventDefault } as any);
+ expect(preventDefault).toBeCalled();
+
+ expect(wrapper.state().submitting).toBe(true);
+ await promise;
+ expect(wrapper.state().submitting).toBe(false);
+ expect(setSecurityHotspotStatus).toBeCalledWith({
+ hotspot: 'key',
+ status: HotspotStatus.TO_REVIEW
+ });
+ expect(onSubmit).toBeCalled();
+
+ // SAFE
+ wrapper.setState({ selectedOption: HotspotStatusOptions.SAFE });
+ await waitAndUpdate(wrapper);
+ await wrapper.instance().handleSubmit({ preventDefault } as any);
+ expect(setSecurityHotspotStatus).toBeCalledWith({
+ hotspot: 'key',
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.SAFE
+ });
+
+ // FIXED
+ wrapper.setState({ selectedOption: HotspotStatusOptions.FIXED });
+ await waitAndUpdate(wrapper);
+ await wrapper.instance().handleSubmit({ preventDefault } as any);
+ expect(setSecurityHotspotStatus).toBeCalledWith({
+ hotspot: 'key',
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.FIXED
+ });
+});
+
+it('should handle submit failure', async () => {
+ const onSubmit = jest.fn();
+ (setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure');
+ const wrapper = shallowRender({ onSubmit });
+ const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);
+ expect(wrapper.state().submitting).toBe(true);
+ await promise.catch(() => {});
+ expect(wrapper.state().submitting).toBe(false);
+ expect(onSubmit).not.toBeCalled();
+});
+
+function shallowRender(props: Partial<HotspotActionsForm['props']> = {}) {
+ return shallow<HotspotActionsForm>(
+ <HotspotActionsForm hotspotKey="key" onSubmit={jest.fn()} {...props} />
+ );
+}
--- /dev/null
+/*
+ * 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 { HotspotStatusOptions } 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: HotspotStatusOptions.SAFE })).toMatchSnapshot(
+ 'safe option selected'
+ );
+});
+
+function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) {
+ return shallow<HotspotActionsForm>(
+ <HotspotActionsFormRenderer
+ hotspotKey="key"
+ onSelectOption={jest.fn()}
+ onSubmit={jest.fn()}
+ selectedOption={HotspotStatusOptions.FIXED}
+ submitting={false}
+ {...props}
+ />
+ );
+}
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockUser } from '../../../../helpers/testMocks';
-import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';
+import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks';
+import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';
it('should render correctly', () => {
const wrapper = shallowRender();
expect(
shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) })
).toMatchSnapshot('deleted assignee');
+ expect(shallowRender()).toMatchSnapshot('anonymous user');
+ expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in');
});
function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
+ currentUser={mockCurrentUser()}
hotspot={mockDetailledHotspot()}
loading={false}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should open when clicked 1`] = `
+<div
+ className="dropdown"
+>
+ <Button
+ onClick={[Function]}
+ >
+ hotspots.review_hotspot
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ <OutsideClickHandler
+ onClickOutside={[Function]}
+ >
+ <DropdownOverlay
+ placement="bottom-right"
+ >
+ <HotspotActionsForm
+ hotspotKey="key"
+ onSubmit={[Function]}
+ />
+ </DropdownOverlay>
+ </OutsideClickHandler>
+</div>
+`;
+
+exports[`should register an eventlistener: Dropdown closed 1`] = `
+<div
+ className="dropdown"
+>
+ <Button
+ onClick={[Function]}
+ >
+ hotspots.review_hotspot
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+</div>
+`;
+
+exports[`should register an eventlistener: Dropdown open 1`] = `
+<div
+ className="dropdown"
+>
+ <Button
+ onClick={[Function]}
+ >
+ hotspots.review_hotspot
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ <OutsideClickHandler
+ onClickOutside={[Function]}
+ >
+ <DropdownOverlay
+ placement="bottom-right"
+ >
+ <HotspotActionsForm
+ hotspotKey="key"
+ onSubmit={[Function]}
+ />
+ </DropdownOverlay>
+ </OutsideClickHandler>
+</div>
+`;
+
+exports[`should register an eventlistener: Dropdown still open 1`] = `
+<div
+ className="dropdown"
+>
+ <Button
+ onClick={[Function]}
+ >
+ hotspots.review_hotspot
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ <OutsideClickHandler
+ onClickOutside={[Function]}
+ >
+ <DropdownOverlay
+ placement="bottom-right"
+ >
+ <HotspotActionsForm
+ hotspotKey="key"
+ onSubmit={[Function]}
+ />
+ </DropdownOverlay>
+ </OutsideClickHandler>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="dropdown"
+>
+ <Button
+ onClick={[Function]}
+ >
+ hotspots.review_hotspot
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<HotspotActionsFormRenderer
+ hotspotKey="key"
+ onSelectOption={[Function]}
+ onSubmit={[Function]}
+ selectedOption="FIXED"
+ submitting={false}
+/>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<form
+ className="abs-width-400"
+ onSubmit={[MockFunction]}
+>
+ <h2>
+ hotspots.form.title
+ </h2>
+ <div
+ className="display-flex-column big-spacer-bottom"
+ >
+ <div
+ className="big-spacer-top"
+ >
+ <Radio
+ checked={true}
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <h3>
+ 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}
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <h3>
+ 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}
+ onCheck={[MockFunction]}
+ value="ADDITIONAL_REVIEW"
+ >
+ <h3>
+ hotspots.status_option.ADDITIONAL_REVIEW
+ </h3>
+ </Radio>
+ <div
+ className="radio-button-description"
+ >
+ hotspots.status_option.ADDITIONAL_REVIEW.description
+ </div>
+ </div>
+ </div>
+ <div
+ className="text-right"
+ >
+ <SubmitButton
+ disabled={false}
+ >
+ hotspots.form.submit
+ </SubmitButton>
+ </div>
+</form>
+`;
+
+exports[`should render correctly: Submitting 1`] = `
+<form
+ className="abs-width-400"
+ onSubmit={[MockFunction]}
+>
+ <h2>
+ hotspots.form.title
+ </h2>
+ <div
+ className="display-flex-column big-spacer-bottom"
+ >
+ <div
+ className="big-spacer-top"
+ >
+ <Radio
+ checked={true}
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <h3>
+ 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}
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <h3>
+ 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}
+ onCheck={[MockFunction]}
+ value="ADDITIONAL_REVIEW"
+ >
+ <h3>
+ hotspots.status_option.ADDITIONAL_REVIEW
+ </h3>
+ </Radio>
+ <div
+ className="radio-button-description"
+ >
+ hotspots.status_option.ADDITIONAL_REVIEW.description
+ </div>
+ </div>
+ </div>
+ <div
+ className="text-right"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <SubmitButton
+ disabled={true}
+ >
+ hotspots.form.submit
+ </SubmitButton>
+ </div>
+</form>
+`;
+
+exports[`should render correctly: safe option selected 1`] = `
+<form
+ className="abs-width-400"
+ onSubmit={[MockFunction]}
+>
+ <h2>
+ hotspots.form.title
+ </h2>
+ <div
+ className="display-flex-column big-spacer-bottom"
+ >
+ <div
+ className="big-spacer-top"
+ >
+ <Radio
+ checked={false}
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <h3>
+ 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}
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <h3>
+ 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}
+ onCheck={[MockFunction]}
+ value="ADDITIONAL_REVIEW"
+ >
+ <h3>
+ hotspots.status_option.ADDITIONAL_REVIEW
+ </h3>
+ </Radio>
+ <div
+ className="radio-button-description"
+ >
+ hotspots.status_option.ADDITIONAL_REVIEW.description
+ </div>
+ </div>
+ </div>
+ <div
+ className="text-right"
+ >
+ <SubmitButton
+ disabled={false}
+ >
+ hotspots.form.submit
+ </SubmitButton>
+ </div>
+</form>
+`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-<HotspotViewerRenderer
+<Connect(withCurrentUser(HotspotViewerRenderer))
loading={true}
securityCategories={
Object {
`;
exports[`should render correctly 2`] = `
-<HotspotViewerRenderer
+<Connect(withCurrentUser(HotspotViewerRenderer))
hotspot={
Object {
"id": "I am a detailled hotspot",
<div
className="big-spacer-bottom"
>
- <h1>
- '3' is a magic number.
- </h1>
+ <div
+ className="display-flex-space-between"
+ >
+ <h1>
+ '3' is a magic number.
+ </h1>
+ </div>
+ <div
+ className="text-muted"
+ >
+ <span>
+ hotspot.category
+ </span>
+ <span
+ className="little-spacer-left"
+ >
+ SQL injection
+ </span>
+ </div>
+ </div>
+ <div
+ className="huge-spacer-bottom"
+ >
+ <span>
+ hotspot.status
+ </span>
+ <span
+ className="badge little-spacer-left"
+ >
+ issue.status.RESOLVED
+ </span>
+ <span
+ className="huge-spacer-left"
+ >
+ hotspot.assigned_to
+ </span>
+ <strong
+ className="little-spacer-left"
+ >
+ John Doe
+ </strong>
+ </div>
+ <HotspotViewerTabs
+ hotspot={
+ Object {
+ "assignee": Object {
+ "active": true,
+ "local": true,
+ "login": "john.doe",
+ "name": "John Doe",
+ },
+ "author": Object {
+ "active": true,
+ "local": true,
+ "login": "john.doe",
+ "name": "John Doe",
+ },
+ "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": "FALSE-POSITIVE",
+ "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": "RESOLVED",
+ "textRange": Object {
+ "endLine": 142,
+ "endOffset": 83,
+ "startLine": 142,
+ "startOffset": 26,
+ },
+ "updateDate": "2013-05-13T17:55:42+0200",
+ }
+ }
+ />
+ </div>
+</DeferredSpinner>
+`;
+
+exports[`should render correctly: anonymous user 1`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <div
+ className="big-padded"
+ >
+ <div
+ className="big-spacer-bottom"
+ >
+ <div
+ className="display-flex-space-between"
+ >
+ <h1>
+ '3' is a magic number.
+ </h1>
+ </div>
<div
className="text-muted"
>
<div
className="big-spacer-bottom"
>
- <h1>
- '3' is a magic number.
- </h1>
+ <div
+ className="display-flex-space-between"
+ >
+ <h1>
+ '3' is a magic number.
+ </h1>
+ </div>
<div
className="text-muted"
>
timeout={100}
/>
`;
+
+exports[`should render correctly: user logged in 1`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <div
+ className="big-padded"
+ >
+ <div
+ className="big-spacer-bottom"
+ >
+ <div
+ className="display-flex-space-between"
+ >
+ <h1>
+ '3' is a magic number.
+ </h1>
+ <HotspotActions
+ hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ />
+ </div>
+ <div
+ className="text-muted"
+ >
+ <span>
+ hotspot.category
+ </span>
+ <span
+ className="little-spacer-left"
+ >
+ SQL injection
+ </span>
+ </div>
+ </div>
+ <div
+ className="huge-spacer-bottom"
+ >
+ <span>
+ hotspot.status
+ </span>
+ <span
+ className="badge little-spacer-left"
+ >
+ issue.status.RESOLVED
+ </span>
+ <span
+ className="huge-spacer-left"
+ >
+ hotspot.assigned_to
+ </span>
+ <strong
+ className="little-spacer-left"
+ >
+ John Doe
+ </strong>
+ </div>
+ <HotspotViewerTabs
+ hotspot={
+ Object {
+ "assignee": Object {
+ "active": true,
+ "local": true,
+ "login": "john.doe",
+ "name": "John Doe",
+ },
+ "author": Object {
+ "active": true,
+ "local": true,
+ "login": "john.doe",
+ "name": "John Doe",
+ },
+ "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": "FALSE-POSITIVE",
+ "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": "RESOLVED",
+ "textRange": Object {
+ "endLine": 142,
+ "endOffset": 83,
+ "startLine": 142,
+ "startOffset": 26,
+ },
+ "updateDate": "2013-05-13T17:55:42+0200",
+ }
+ }
+ />
+ </div>
+</DeferredSpinner>
+`;
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;
+}
HIGH = 'HIGH'
}
+export enum HotspotStatus {
+ TO_REVIEW = 'TO_REVIEW',
+ REVIEWED = 'REVIEWED'
+}
+
+export enum HotspotResolution {
+ FIXED = 'FIXED',
+ SAFE = 'SAFE'
+}
+
+export enum HotspotStatusOptions {
+ FIXED = 'FIXED',
+ SAFE = 'SAFE',
+ ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW'
+}
+
export interface RawHotspot {
assignee?: string;
author?: string;
hotspots: RawHotspot[];
paging: T.Paging;
}
+
+export interface HotspotSetStatusRequest {
+ hotspot: string;
+ status: HotspotStatus;
+ resolution?: HotspotResolution;
+}
hotspot.tabs.risk_description=What's the risk?
hotspot.tabs.vulnerability_description=Are you vulnerable?
hotspot.tabs.fix_recommendations=How can you fix it?
+hotspots.review_hotspot=Review Hotspot
+
+hotspots.form.title=Mark Security Hotspot as:
+
+hotspots.form.assign_to=Assign to:
+hotspots.form.select_user=Select a user...
+hotspots.form.comment=Comment
+hotspots.form.submit=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.
#------------------------------------------------------------------------------
#