@@ -440,6 +440,10 @@ th.huge-spacer-right { | |||
flex: 1; | |||
} | |||
.flex-1-0-auto { | |||
flex: 1 0 auto; | |||
} | |||
.flex-0 { | |||
flex: 0 0 auto; | |||
} |
@@ -119,7 +119,7 @@ export default class HotspotList extends React.Component<Props, State> { | |||
{groupedHotspots.map(riskGroup => ( | |||
<li className="big-spacer-bottom" key={riskGroup.risk}> | |||
<div className="hotspot-risk-header little-spacer-left"> | |||
<span>{translate('hotspots.risk_exposure')}</span> | |||
<span>{translate('hotspots.risk_exposure')}:</span> | |||
<div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}> | |||
{translate('risk_exposure', riskGroup.risk)} | |||
</div> |
@@ -0,0 +1,27 @@ | |||
/* | |||
* 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. | |||
*/ | |||
.hotspot-information { | |||
flex-basis: 320px; | |||
} | |||
.hotspot-information > div > span { | |||
flex-basis: 100px; | |||
} |
@@ -17,6 +17,7 @@ | |||
* 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 { ClipboardButton } from 'sonar-ui-common/components/controls/clipboard'; | |||
import LinkIcon from 'sonar-ui-common/components/icons/LinkIcon'; | |||
@@ -29,6 +30,7 @@ import { BranchLike } from '../../../types/branch-like'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import Assignee from './assignee/Assignee'; | |||
import HotspotSnippetContainer from './HotspotSnippetContainer'; | |||
import './HotspotViewer.css'; | |||
import HotspotViewerTabs from './HotspotViewerTabs'; | |||
import Status from './status/Status'; | |||
@@ -56,25 +58,44 @@ export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) | |||
<DeferredSpinner loading={loading}> | |||
{hotspot && ( | |||
<div className="big-padded"> | |||
<div className="big-spacer-bottom"> | |||
<div className="display-flex-space-between"> | |||
<strong className="big">{hotspot.message}</strong> | |||
<ClipboardButton copyValue={permalink}> | |||
<LinkIcon className="spacer-right" /> | |||
<span>{translate('hotspots.get_permalink')}</span> | |||
</ClipboardButton> | |||
<div className="huge-spacer-bottom display-flex-space-between"> | |||
<strong className="big big-spacer-right">{hotspot.message}</strong> | |||
<ClipboardButton copyValue={permalink}> | |||
<LinkIcon className="spacer-right" /> | |||
<span>{translate('hotspots.get_permalink')}</span> | |||
</ClipboardButton> | |||
</div> | |||
<div className="huge-spacer-bottom display-flex-row"> | |||
<div className="hotspot-information display-flex-column display-flex-space-between"> | |||
<div className="display-flex-center"> | |||
<span className="big-spacer-right">{translate('category')}</span> | |||
<strong className="nowrap"> | |||
{securityCategories[hotspot.rule.securityCategory].title} | |||
</strong> | |||
</div> | |||
<div className="display-flex-center"> | |||
<span className="big-spacer-right">{translate('hotspots.risk_exposure')}</span> | |||
<div | |||
className={classNames( | |||
'hotspot-risk-badge', | |||
hotspot.rule.vulnerabilityProbability | |||
)}> | |||
{translate('risk_exposure', hotspot.rule.vulnerabilityProbability)} | |||
</div> | |||
</div> | |||
<div className="display-flex-center"> | |||
<span className="big-spacer-right">{translate('assignee')}</span> | |||
<div> | |||
<Assignee hotspot={hotspot} onAssigneeChange={props.onUpdateHotspot} /> | |||
</div> | |||
</div> | |||
</div> | |||
<div className="text-muted"> | |||
<span>{translate('category')}:</span> | |||
<span className="little-spacer-left"> | |||
{securityCategories[hotspot.rule.securityCategory].title} | |||
</span> | |||
<div className="huge-spacer-left"> | |||
<Status hotspot={hotspot} onStatusChange={props.onUpdateHotspot} /> | |||
</div> | |||
</div> | |||
<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} /> | |||
</div> |
@@ -92,6 +92,7 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
> | |||
<span> | |||
hotspots.risk_exposure | |||
: | |||
</span> | |||
<div | |||
className="hotspot-risk-badge spacer-left HIGH" | |||
@@ -205,6 +206,7 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
> | |||
<span> | |||
hotspots.risk_exposure | |||
: | |||
</span> | |||
<div | |||
className="hotspot-risk-badge spacer-left MEDIUM" | |||
@@ -357,6 +359,7 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
> | |||
<span> | |||
hotspots.risk_exposure | |||
: | |||
</span> | |||
<div | |||
className="hotspot-risk-badge spacer-left HIGH" | |||
@@ -470,6 +473,7 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
> | |||
<span> | |||
hotspots.risk_exposure | |||
: | |||
</span> | |||
<div | |||
className="hotspot-risk-badge spacer-left MEDIUM" |
@@ -43,39 +43,33 @@ export default function AssigneeRenderer(props: AssigneeRendererProps) { | |||
const { assignee, canEdit, loggedInUser, editing, loading } = props; | |||
return ( | |||
<div className="big-spacer-top display-flex-center"> | |||
<span>{translate('assignee')}:</span> | |||
<span className="spacer-left"> | |||
<DeferredSpinner loading={loading}> | |||
{!editing && ( | |||
<div className="display-flex-center"> | |||
<strong> | |||
{assignee && | |||
(assignee.active | |||
? assignee.name ?? assignee.login | |||
: translateWithParameters('user.x_deleted', assignee.name ?? assignee.login))} | |||
{!assignee && translate('unassigned')} | |||
</strong> | |||
{loggedInUser && canEdit && ( | |||
<EditButton className="spacer-left" onClick={props.onEnterEditionMode} /> | |||
)} | |||
</div> | |||
<DeferredSpinner loading={loading}> | |||
{!editing && ( | |||
<div className="display-flex-center"> | |||
<strong className="nowrap"> | |||
{assignee && | |||
(assignee.active | |||
? assignee.name ?? assignee.login | |||
: translateWithParameters('user.x_deleted', assignee.name ?? assignee.login))} | |||
{!assignee && translate('unassigned')} | |||
</strong> | |||
{loggedInUser && canEdit && ( | |||
<EditButton className="spacer-left" onClick={props.onEnterEditionMode} /> | |||
)} | |||
</div> | |||
)} | |||
{loggedInUser && editing && ( | |||
<EscKeydownHandler onKeydown={props.onExitEditionMode}> | |||
<OutsideClickHandler onClickOutside={props.onExitEditionMode}> | |||
<AssigneeSelection | |||
allowCurrentUserSelection={loggedInUser.login !== assignee?.login} | |||
loggedInUser={loggedInUser} | |||
onSelect={props.onAssign} | |||
/> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
)} | |||
</DeferredSpinner> | |||
</span> | |||
</div> | |||
{loggedInUser && editing && ( | |||
<EscKeydownHandler onKeydown={props.onExitEditionMode}> | |||
<OutsideClickHandler onClickOutside={props.onExitEditionMode}> | |||
<AssigneeSelection | |||
allowCurrentUserSelection={loggedInUser.login !== assignee?.login} | |||
loggedInUser={loggedInUser} | |||
onSelect={props.onAssign} | |||
/> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
)} | |||
</DeferredSpinner> | |||
); | |||
} |
@@ -17,6 +17,11 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.hotspot-assignee-search-results { | |||
min-width: 300px; | |||
} | |||
.hotspot-assignee-search-results li { | |||
cursor: pointer; | |||
} |
@@ -56,10 +56,7 @@ export default function AssigneeSelectionRenderer(props: HotspotAssigneeSelectRe | |||
{!loading && open && ( | |||
<div className="position-relative"> | |||
<DropdownOverlay | |||
className="abs-width-400" | |||
noPadding={true} | |||
placement={PopupPlacement.BottomLeft}> | |||
<DropdownOverlay noPadding={true} placement={PopupPlacement.BottomLeft}> | |||
{suggestedUsers && suggestedUsers.length > 0 ? ( | |||
<ul className="hotspot-assignee-search-results"> | |||
{suggestedUsers.map(suggestion => ( |
@@ -1,135 +1,93 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: editing 1`] = ` | |||
<div | |||
className="big-spacer-top display-flex-center" | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<span> | |||
assignee | |||
: | |||
</span> | |||
<span | |||
className="spacer-left" | |||
<EscKeydownHandler | |||
onKeydown={[MockFunction]} | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
<OutsideClickHandler | |||
onClickOutside={[MockFunction]} | |||
> | |||
<EscKeydownHandler | |||
onKeydown={[MockFunction]} | |||
> | |||
<OutsideClickHandler | |||
onClickOutside={[MockFunction]} | |||
> | |||
<AssigneeSelection | |||
allowCurrentUserSelection={false} | |||
loggedInUser={ | |||
Object { | |||
"groups": Array [], | |||
"isLoggedIn": true, | |||
"login": "luke", | |||
"name": "Skywalker", | |||
"scmAccounts": Array [], | |||
} | |||
} | |||
onSelect={[MockFunction]} | |||
/> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
</DeferredSpinner> | |||
</span> | |||
</div> | |||
<AssigneeSelection | |||
allowCurrentUserSelection={false} | |||
loggedInUser={ | |||
Object { | |||
"groups": Array [], | |||
"isLoggedIn": true, | |||
"login": "luke", | |||
"name": "Skywalker", | |||
"scmAccounts": Array [], | |||
} | |||
} | |||
onSelect={[MockFunction]} | |||
/> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
</DeferredSpinner> | |||
`; | |||
exports[`should render correctly: not editing 1`] = ` | |||
<div | |||
className="big-spacer-top display-flex-center" | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<span> | |||
assignee | |||
: | |||
</span> | |||
<span | |||
className="spacer-left" | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
<strong | |||
className="nowrap" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<strong> | |||
user.x_deleted.Skywalker | |||
</strong> | |||
<EditButton | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
</span> | |||
</div> | |||
user.x_deleted.Skywalker | |||
</strong> | |||
<EditButton | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; | |||
exports[`should render correctly: with active assignee 1`] = ` | |||
<div | |||
className="big-spacer-top display-flex-center" | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<span> | |||
assignee | |||
: | |||
</span> | |||
<span | |||
className="spacer-left" | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
<strong | |||
className="nowrap" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<strong> | |||
John Doe | |||
</strong> | |||
<EditButton | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
</span> | |||
</div> | |||
John Doe | |||
</strong> | |||
<EditButton | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; | |||
exports[`should render correctly: without current assignee 1`] = ` | |||
<div | |||
className="big-spacer-top display-flex-center" | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<span> | |||
assignee | |||
: | |||
</span> | |||
<span | |||
className="spacer-left" | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
<strong | |||
className="nowrap" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<strong> | |||
unassigned | |||
</strong> | |||
<EditButton | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
</span> | |||
</div> | |||
unassigned | |||
</strong> | |||
<EditButton | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; |
@@ -50,7 +50,6 @@ exports[`should render correctly: open 1`] = ` | |||
className="position-relative" | |||
> | |||
<DropdownOverlay | |||
className="abs-width-400" | |||
noPadding={true} | |||
placement="bottom-left" | |||
> | |||
@@ -80,7 +79,6 @@ exports[`should render correctly: open with results 1`] = ` | |||
className="position-relative" | |||
> | |||
<DropdownOverlay | |||
className="abs-width-400" | |||
noPadding={true} | |||
placement="bottom-left" | |||
> |
@@ -19,7 +19,7 @@ | |||
*/ | |||
#status-trigger, | |||
.popup { | |||
#status-trigger + .popup { | |||
width: 400px; | |||
box-sizing: border-box; | |||
} |
@@ -92,7 +92,7 @@ export function Status(props: StatusProps) { | |||
); | |||
return ( | |||
<div className="dropdown huge-spacer-left"> | |||
<div className="dropdown"> | |||
{readonly ? ( | |||
<Tooltip overlay={translate('hotspots.status.cannot_change_status')} placement="bottom"> | |||
{actionableTrigger} |
@@ -2,7 +2,7 @@ | |||
exports[`should render correctly: closed 1`] = ` | |||
<div | |||
className="dropdown huge-spacer-left" | |||
className="dropdown" | |||
> | |||
<Toggler | |||
closeOnClickOutside={true} | |||
@@ -146,7 +146,7 @@ exports[`should render correctly: closed 1`] = ` | |||
exports[`should render correctly: open 1`] = ` | |||
<div | |||
className="dropdown huge-spacer-left" | |||
className="dropdown" | |||
> | |||
<Toggler | |||
closeOnClickOutside={true} | |||
@@ -291,7 +291,7 @@ exports[`should render correctly: open 1`] = ` | |||
exports[`should render correctly: readonly 1`] = ` | |||
<div | |||
className="dropdown huge-spacer-left" | |||
className="dropdown" | |||
> | |||
<Tooltip | |||
overlay="hotspots.status.cannot_change_status" |
@@ -662,7 +662,7 @@ hotspots.list_title={0} Security Hotspots | |||
hotspots.list_title.TO_REVIEW={0} Security Hotspots to review | |||
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: | |||
hotspots.risk_exposure=Review priority | |||
hotspots.tabs.risk_description=What's the risk? | |||
hotspots.tabs.vulnerability_description=Are you at risk? |