From cb7e1bc3b72d71c19fdb5952c3089afa62c99b85 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Tue, 23 May 2023 15:34:25 +0200 Subject: [PATCH] SONAR-19236 Implement new source viewer header for security hotspots --- .../design-system/src/theme/light.ts | 2 + .../__tests__/SecurityHotspotsApp-it.tsx | 59 +++++++++----- .../components/HotspotOpenInIdeButton.tsx | 64 ++++++++++----- .../components/HotspotOpenInIdeOverlay.tsx | 44 ---------- .../HotspotSnippetContainerRenderer.tsx | 66 ++++++++------- .../components/HotspotSnippetHeader.tsx | 81 +++++-------------- .../components/HotspotViewer.css | 5 -- 7 files changed, 145 insertions(+), 176 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index ea90096897c..f8522d70256 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -192,6 +192,8 @@ export const lightTheme = { codeSnippetInline: COLORS.blueGrey[500], // code viewer + codeLine: COLORS.white, + codeLineBorder: COLORS.grey[100], codeLineIssueIndicator: COLORS.blueGrey[400], // Should be blueGrey[300], to be changed once code viewer is reworked codeLineLocationMarker: COLORS.red[200], codeLineLocationMarkerSelected: danger.lighter, diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx index 67793addd70..8f09fcc0e4b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx @@ -28,6 +28,7 @@ import { getSecurityHotspots, setSecurityHotspotStatus } from '../../../api/secu import { searchUsers } from '../../../api/users'; import { mockBranch, mockMainBranch } from '../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../helpers/mocks/component'; +import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint'; import { get, save } from '../../../helpers/storage'; import { mockLoggedInUser } from '../../../helpers/testMocks'; import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils'; @@ -45,6 +46,16 @@ jest.mock('../../../api/rules'); jest.mock('../../../api/quality-profiles'); jest.mock('../../../api/issues'); jest.mock('../hooks/useScrollDownCompress'); +jest.mock('../../../helpers/sonarlint', () => ({ + openHotspot: jest.fn().mockResolvedValue(null), + probeSonarLintServers: jest.fn().mockResolvedValue([ + { + port: 1234, + ideName: 'VIM', + description: 'I use VIM', + }, + ]), +})); jest.mock('.../../../helpers/storage'); const ui = { @@ -85,6 +96,7 @@ const ui = { showAllHotspotLink: byRole('link', { name: 'hotspot.filters.show_all' }), activityTab: byRole('tab', { name: /hotspots.tabs.activity/ }), addCommentButton: byRole('button', { name: 'hotspots.status.add_comment' }), + openInIDEButton: byRole('button', { name: 'hotspots.open_in_ide.open' }), continueReviewingButton: byRole('button', { name: 'hotspots.continue_to_next_hotspot' }), seeStatusHotspots: byRole('button', { name: /hotspots.see_x_hotspots/ }), dontShowSuccessDialogCheckbox: byRole('checkbox', { @@ -170,24 +182,6 @@ describe('rendering', () => { }); }); -it('should navigate when comming from SonarLint', async () => { - // On main branch - const rtl = renderSecurityHotspotsApp( - 'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=test-1' - ); - - expect(await ui.hotspotTitle(/'3' is a magic number./).find()).toBeInTheDocument(); - - // On specific branch - rtl.unmount(); - renderSecurityHotspotsApp( - 'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=b1-test-1&branch=b1', - { branchLike: mockBranch({ name: 'b1' }) } - ); - - expect(await ui.hotspotTitle(/'F' is a magic number./).find()).toBeInTheDocument(); -}); - describe('CRUD', () => { it('should be able to self-assign a hotspot', async () => { const user = userEvent.setup(); @@ -361,6 +355,35 @@ describe('navigation', () => { expect(await ui.hotspotTitle(/'F' is a magic number./).find()).toBeInTheDocument(); }); + + it('should allow to open a hotspot in an IDE', async () => { + const user = userEvent.setup(); + renderSecurityHotspotsApp(); + + await user.click(await ui.openInIDEButton.find()); + expect(openHotspot).toHaveBeenCalledWith(1234, 'hotspot-component', 'test-1'); + }); + + it('should allow to choose in which IDE to open a hotspot', async () => { + jest.mocked(probeSonarLintServers).mockResolvedValueOnce([ + { + port: 1234, + ideName: 'VIM', + description: 'I use VIM', + }, + { + port: 4567, + ideName: 'MS Paint', + description: 'I use MS Paint cuz Ima boss', + }, + ]); + const user = userEvent.setup(); + renderSecurityHotspotsApp(); + + await user.click(await ui.openInIDEButton.find()); + await user.click(screen.getByRole('menuitem', { name: /MS Paint/ })); + expect(openHotspot).toHaveBeenCalledWith(4567, 'hotspot-component', 'test-1'); + }); }); it('after status change, should be able to disable success dialog show', async () => { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx index 0b5e603d22b..6963b7cb28a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx @@ -17,16 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + ButtonSecondary, + DropdownMenu, + DropdownToggler, + ItemButton, + PopupPlacement, +} from 'design-system'; import * as React from 'react'; -import { Button } from '../../../components/controls/buttons'; -import { DropdownOverlay } from '../../../components/controls/Dropdown'; -import Toggler from '../../../components/controls/Toggler'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages'; import { translate } from '../../../helpers/l10n'; import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint'; import { Ide } from '../../../types/sonarlint'; -import { HotspotOpenInIdeOverlay } from './HotspotOpenInIdeOverlay'; interface Props { projectKey: string; @@ -35,7 +38,7 @@ interface Props { interface State { loading: boolean; - ides: Array; + ides: Ide[]; } export default class HotspotOpenInIdeButton extends React.PureComponent { @@ -43,7 +46,7 @@ export default class HotspotOpenInIdeButton extends React.PureComponent { - this.setState({ loading: true, ides: [] }); + this.setState({ loading: true, ides: [] as Ide[] }); const { projectKey, hotspotKey } = this.props; return openHotspot(ide.port, projectKey, hotspotKey) .then(this.showSuccess) @@ -89,21 +92,40 @@ export default class HotspotOpenInIdeButton extends React.PureComponent 1} - onRequestClose={this.cleanState} - overlay={ - - - - } - > - - +
+ this.cleanState()} + allowResizing={true} + open={ides.length > 1} + placement={PopupPlacement.BottomLeft} + isPortal={true} + overlay={ + + {ides.map((ide) => { + const { ideName, description } = ide; + const label = ideName + (description ? ` - ${description}` : ''); + return ( + { + this.openHotspot(ide); + }} + > + {label} + + ); + })} + + } + > + + {translate('hotspots.open_in_ide.open')} + + + +
); } } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx deleted file mode 100644 index 2d18f820080..00000000000 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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 { Ide } from '../../../types/sonarlint'; - -export const HotspotOpenInIdeOverlay = ({ - ides, - onIdeSelected, -}: { - ides: Array; - onIdeSelected: (ide: Ide) => Promise; -}) => - ides.length > 1 ? ( -
    - {ides.map((ide) => { - const { ideName, description } = ide; - const label = ideName + (description ? ` - ${description}` : ''); - return ( -
  • - onIdeSelected(ide)}> - {label} - -
  • - ); - })} -
- ) : null; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx index 54be3aa36c8..78a329c7ddd 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx @@ -17,6 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { withTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { themeBorder, themeColor } from 'design-system'; import * as React from 'react'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; @@ -146,35 +149,42 @@ export default function HotspotSnippetContainerRenderer( return ( <> {!loading && sourceLines.length === 0 && ( -

{translate('hotspots.no_associated_lines')}

+

{translate('hotspots.no_associated_lines')}

)} - -
- - {sourceLines.length > 0 && ( - - animateExpansion(scrollableRef, props.onExpandBlock, direction) - } - handleSymbolClick={props.onSymbolClick} - highlightedLocationMessage={highlightedLocation} - highlightedSymbols={highlightedSymbols} - index={0} - issue={hotspot} - lastSnippetOfLastGroup={false} - locations={secondaryLocations} - locationsByLine={primaryLocations} - onLocationSelect={props.onLocationSelect} - renderAdditionalChildInLine={renderHotspotBoxInLine} - renderDuplicationPopup={noop} - snippet={sourceLines} - /> - )} - -
+ + + + + + + {!loading && sourceLines.length > 0 && ( + + animateExpansion(scrollableRef, props.onExpandBlock, direction) + } + handleSymbolClick={props.onSymbolClick} + highlightedLocationMessage={highlightedLocation} + highlightedSymbols={highlightedSymbols} + index={0} + issue={hotspot} + lastSnippetOfLastGroup={false} + locations={secondaryLocations} + locationsByLine={primaryLocations} + onLocationSelect={props.onLocationSelect} + renderAdditionalChildInLine={renderHotspotBoxInLine} + renderDuplicationPopup={noop} + snippet={sourceLines} + /> + )} + ); } + +const SourceFileWrapper = withTheme(styled.div` + background-color: ${themeColor('codeLine')}; + border: ${themeBorder('default', 'codeLineBorder')}; +`); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetHeader.tsx index 4c74d63fa9b..7b75a6e90f8 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetHeader.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetHeader.tsx @@ -17,17 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { ClipboardIconButton, HoverLink, Note, themeBorder, themeColor } from 'design-system'; import React from 'react'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; -import { colors, sizes } from '../../../app/theme'; -import { ClipboardButton, ClipboardIconButton } from '../../../components/controls/clipboard'; -import LinkIcon from '../../../components/icons/LinkIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; -import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; -import { getComponentSecurityHotspotsUrl, getPathUrlAsString } from '../../../helpers/urls'; +import { getBranchLikeUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; import { Hotspot } from '../../../types/security-hotspots'; @@ -42,7 +40,7 @@ export interface HotspotSnippetHeaderProps { branchLike?: BranchLike; } -export function HotspotSnippetHeader(props: HotspotSnippetHeaderProps) { +function HotspotSnippetHeader(props: HotspotSnippetHeaderProps) { const { hotspot, currentUser, component, branchLike } = props; const { project, @@ -51,76 +49,39 @@ export function HotspotSnippetHeader(props: HotspotSnippetHeaderProps) { const displayProjectName = component.qualifier === ComponentQualifier.Application; - const permalink = getPathUrlAsString( - getComponentSecurityHotspotsUrl(component.key, { - ...getBranchLikeQuery(branchLike), - hotspots: hotspot?.key, - }), - false - ); - return ( - - + + {displayProjectName && ( - <> - - + + + {project.name} - - + + )} - + {collapsedDirFromPath(path)} {fileFromPath(path)} + - + {isLoggedIn(currentUser) && ( -
- -
+ )} - - - {translate('hotspots.get_permalink')} - - -
+ ); } -const Container = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - height: 40px; - padding: 0 ${sizes.gridSize}; - border: 1px solid ${colors.barBorderColor}; - background-color: ${colors.barBackgroundColor}; -`; - -const FilePath = styled.div` - display: flex; - align-items: center; - margin-right: 40px; - color: ${colors.secondFontColor}; - svg { - margin: 0 ${sizes.gridSize}; - } -`; - -const ProjectName = styled.span` - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - direction: rtl; - white-space: nowrap; -`; +const StyledHeader = withTheme(styled.div` + background-color: ${themeColor('codeLine')}; + border-bottom: ${themeBorder('default', 'codeLineBorder')}; +`); export default withCurrentUserContext(HotspotSnippetHeader); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.css index c2229d59a89..f4b2db32b94 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.css +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.css @@ -25,11 +25,6 @@ flex-basis: 100px; } -.hotspot-snippet-container { - max-height: calc(100vh - 500px); - overflow: auto; -} - .hotspot-content .markdown { line-height: 1.8; } -- 2.39.5