diff options
author | David Cho-Lerat <david.cho-lerat@sonarsource.com> | 2023-10-26 15:50:16 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-10-27 20:03:00 +0000 |
commit | 8619068f5c2641beef855e6a7cd412e19de271d8 (patch) | |
tree | a49e5dbe1d5172558898e657025280b87227c3f3 /server/sonar-web | |
parent | e2af95fe351114061ca3409ba1a9ba2abb58ab05 (diff) | |
download | sonarqube-8619068f5c2641beef855e6a7cd412e19de271d8.tar.gz sonarqube-8619068f5c2641beef855e6a7cd412e19de271d8.zip |
SONAR-20447 Pass extra 'branch' parameter to the SonarLint call
Diffstat (limited to 'server/sonar-web')
10 files changed, 125 insertions, 52 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx index 43b8f3c24f4..2c2a3faa56d 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx @@ -33,6 +33,7 @@ import { openIssue as openSonarLintIssue, probeSonarLintServers } from '../../.. import { Ide } from '../../../types/sonarlint'; export interface Props { + branchName?: string; issueKey: string; projectKey: string; } @@ -46,7 +47,7 @@ const showError = () => addGlobalErrorMessage(translate('issues.open_in_ide.fail const showSuccess = () => addGlobalSuccessMessage(translate('issues.open_in_ide.success')); -export function IssueOpenInIdeButton({ issueKey, projectKey }: Readonly<Props>) { +export function IssueOpenInIdeButton({ branchName, issueKey, projectKey }: Readonly<Props>) { const [state, setState] = React.useState<State>({ ides: [], mounted: false }); React.useEffect(() => { @@ -67,7 +68,7 @@ export function IssueOpenInIdeButton({ issueKey, projectKey }: Readonly<Props>) const openIssue = (ide: Ide) => { setState({ ...state, ides: [] }); - return openSonarLintIssue(ide.port, projectKey, issueKey) + return openSonarLintIssue(ide.port, projectKey, issueKey, branchName) .then(showSuccess) .catch(showError) .finally(cleanState); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx index aefd80d017f..f092b21b4d8 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx @@ -103,6 +103,7 @@ it('handles button click with one ide found', async () => { MOCK_IDES[0].port, MOCK_PROJECT_KEY, MOCK_ISSUE_KEY, + undefined, ); expect(addGlobalSuccessMessage).toHaveBeenCalledWith('issues.open_in_ide.success'); @@ -147,6 +148,7 @@ it('handles button click with several ides found', async () => { MOCK_IDES[1].port, MOCK_PROJECT_KEY, MOCK_ISSUE_KEY, + undefined, ); expect(addGlobalSuccessMessage).toHaveBeenCalledWith('issues.open_in_ide.success'); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index 880a385ae41..6505066761a 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -43,7 +43,7 @@ import { Issue as TypeIssue, } from '../../../types/types'; import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext'; -import IssueSourceViewerHeader from './IssueSourceViewerHeader'; +import { IssueSourceViewerHeader } from './IssueSourceViewerHeader'; import SnippetViewer from './SnippetViewer'; import { EXPAND_BY_LINES, @@ -264,7 +264,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone }; render() { - const { branchLike, isLastOccurenceOfPrimaryComponent, issue, snippetGroup } = this.props; + const { isLastOccurenceOfPrimaryComponent, issue, snippetGroup } = this.props; const { additionalLines, loading, snippets } = this.state; const snippetLines = linesForSnippets(snippets, { @@ -302,7 +302,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone )} <IssueSourceViewerHeader - branchLike={branchLike} className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''} expandable={isExpandable(snippets, snippetGroup)} issueKey={issue.key} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx index 49b7d776a63..2f920ddb0f6 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import classNames from 'classnames'; import { @@ -28,31 +29,28 @@ import { LightLabel, Link, Spinner, - ThemeProp, UnfoldIcon, themeColor, - withTheme, } from 'design-system'; import * as React from 'react'; -import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; +import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; +import { useCurrentUser } from '../../../app/components/current-user/CurrentUserContext'; import Tooltip from '../../../components/controls/Tooltip'; import { ClipboardBase } from '../../../components/controls/clipboard'; -import { getBranchLikeQuery } from '../../../helpers/branch-like'; +import { getBranchLikeQuery, isBranch, isPullRequest } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; import { getBranchLikeUrl, getComponentIssuesUrl } from '../../../helpers/urls'; -import { BranchLike } from '../../../types/branch-like'; +import { useBranchesQuery } from '../../../queries/branch'; import { ComponentQualifier } from '../../../types/component'; import { SourceViewerFile } from '../../../types/types'; -import { CurrentUser, isLoggedIn } from '../../../types/users'; +import { isLoggedIn } from '../../../types/users'; import { IssueOpenInIdeButton } from '../components/IssueOpenInIdeButton'; export const INTERACTIVE_TOOLTIP_DELAY = 0.5; export interface Props { - branchLike: BranchLike | undefined; className?: string; - currentUser: CurrentUser; displayProjectName?: boolean; expandable?: boolean; issueKey: string; @@ -62,11 +60,16 @@ export interface Props { sourceViewerFile: SourceViewerFile; } -function IssueSourceViewerHeader(props: Readonly<Props> & ThemeProp) { +export function IssueSourceViewerHeader(props: Readonly<Props>) { + const { component } = React.useContext(ComponentContext); + const { data: branchData, isLoading: isLoadingBranches } = useBranchesQuery(component); + const currentUser = useCurrentUser(); + const theme = useTheme(); + + const branchLike = branchData?.branchLike; + const { - branchLike, className, - currentUser, displayProjectName = true, expandable, issueKey, @@ -74,7 +77,6 @@ function IssueSourceViewerHeader(props: Readonly<Props> & ThemeProp) { loading, onExpand, sourceViewerFile, - theme, } = props; const { measures, path, project, projectName, q } = sourceViewerFile; @@ -88,6 +90,18 @@ function IssueSourceViewerHeader(props: Readonly<Props> & ThemeProp) { border-bottom: none; `; + const branchName = React.useMemo(() => { + if (isBranch(branchLike)) { + return branchLike.name; + } + + if (isPullRequest(branchLike)) { + return branchLike.branch; + } + + return undefined; // should never end up here, but needed for consistent returns + }, [branchLike]); + return ( <IssueSourceViewerStyle aria-label={sourceViewerFile.path} @@ -149,13 +163,13 @@ function IssueSourceViewerHeader(props: Readonly<Props> & ThemeProp) { </div> {!isProjectRoot && isLoggedIn(currentUser) && ( - <IssueOpenInIdeButton issueKey={issueKey} projectKey={project} /> + <IssueOpenInIdeButton branchName={branchName} issueKey={issueKey} projectKey={project} /> )} {!isProjectRoot && measures.issues !== undefined && ( <div className={classNames('sw-ml-4', { - 'sw-mr-1': !expandable || loading, + 'sw-mr-1': (!expandable || loading) ?? isLoadingBranches, })} > <Link @@ -170,9 +184,9 @@ function IssueSourceViewerHeader(props: Readonly<Props> & ThemeProp) { </div> )} - <Spinner className="sw-mr-1" loading={loading} /> + <Spinner className="sw-mr-1" loading={loading ?? isLoadingBranches} /> - {expandable && !loading && ( + {expandable && !(loading ?? isLoadingBranches) && ( <div className="sw-ml-4"> <InteractiveIcon Icon={UnfoldIcon} @@ -185,5 +199,3 @@ function IssueSourceViewerHeader(props: Readonly<Props> & ThemeProp) { </IssueSourceViewerStyle> ); } - -export default withCurrentUserContext(withTheme(IssueSourceViewerHeader)); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx index 484f4c3b187..4121a99e936 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx @@ -17,12 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { screen, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; +import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; +import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; +import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext'; +import { mockComponent } from '../../../../helpers/mocks/component'; import { mockSourceViewerFile } from '../../../../helpers/mocks/sources'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { byRole, byText } from '../../../../helpers/testSelector'; -import IssueSourceViewerHeader, { Props } from '../IssueSourceViewerHeader'; +import { Feature } from '../../../../types/features'; +import { IssueSourceViewerHeader, Props } from '../IssueSourceViewerHeader'; const ui = { expandAllLines: byRole('button', { name: 'source_viewer.expand_all_lines' }), @@ -31,54 +37,87 @@ const ui = { viewAllIssues: byRole('link', { name: 'source_viewer.view_all_issues' }), }; -it('should render correctly', () => { +const branchHandler = new BranchesServiceMock(); + +afterEach(() => { + branchHandler.reset(); +}); + +it('should render correctly', async () => { + branchHandler.emptyBranchesAndPullRequest(); + renderIssueSourceViewerHeader(); + expect(await screen.findByText('loading')).toBeInTheDocument(); + await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument()); + expect(ui.expandAllLines.get()).toBeInTheDocument(); expect(ui.projectLink.get()).toBeInTheDocument(); expect(ui.projectName.get()).toBeInTheDocument(); expect(ui.viewAllIssues.get()).toBeInTheDocument(); }); -it('should not render expandable link', () => { +it('should not render expandable link', async () => { renderIssueSourceViewerHeader({ expandable: false }); + expect(await screen.findByText('loading')).toBeInTheDocument(); + await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument()); + expect(ui.expandAllLines.query()).not.toBeInTheDocument(); }); -it('should not render link to project', () => { - renderIssueSourceViewerHeader({ linkToProject: false }); +it('should not render link to project', async () => { + renderIssueSourceViewerHeader({ linkToProject: false }, '?id=my-project&branch=normal-branch'); + + expect(await screen.findByText('loading')).toBeInTheDocument(); + await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument()); expect(ui.projectLink.query()).not.toBeInTheDocument(); expect(ui.projectName.get()).toBeInTheDocument(); }); -it('should not render project name', () => { - renderIssueSourceViewerHeader({ displayProjectName: false }); +it('should not render project name', async () => { + renderIssueSourceViewerHeader({ displayProjectName: false }, '?id=my-project&pullRequest=01'); + + expect(await screen.findByText('loading')).toBeInTheDocument(); + await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument()); expect(ui.projectLink.query()).not.toBeInTheDocument(); expect(ui.projectName.query()).not.toBeInTheDocument(); }); -it('should render without issue expand all when no issue', () => { +it('should render without issue expand all when no issue', async () => { renderIssueSourceViewerHeader({ sourceViewerFile: mockSourceViewerFile('foo/bar.ts', 'my-project', { measures: {}, }), }); + expect(await screen.findByText('loading')).toBeInTheDocument(); + await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument()); + expect(ui.viewAllIssues.query()).not.toBeInTheDocument(); }); -function renderIssueSourceViewerHeader(props: Partial<Props> = {}) { +function renderIssueSourceViewerHeader(props: Partial<Props> = {}, path = '?id=my-project') { return renderComponent( - <IssueSourceViewerHeader - branchLike={mockMainBranch()} - expandable - issueKey="issue-key" - onExpand={jest.fn()} - sourceViewerFile={mockSourceViewerFile('foo/bar.ts', 'my-project')} - {...props} - />, + <AvailableFeaturesContext.Provider value={[Feature.BranchSupport]}> + <ComponentContext.Provider + value={{ + component: mockComponent(), + onComponentChange: jest.fn(), + fetchComponent: jest.fn(), + }} + > + <IssueSourceViewerHeader + expandable + issueKey="issue-key" + onExpand={jest.fn()} + sourceViewerFile={mockSourceViewerFile('foo/bar.ts', 'my-project')} + {...props} + /> + </ComponentContext.Provider> + </AvailableFeaturesContext.Provider>, + path, ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx index 0579f88f06b..ec96cfcf2ef 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx @@ -54,7 +54,7 @@ export interface HotspotHeaderProps { export function HotspotHeader(props: HotspotHeaderProps) { const { branchLike, component, hotspot, standards } = props; const { message, messageFormattings, rule, key } = hotspot; - const refrechBranchStatus = useRefreshBranchStatus(); + const refreshBranchStatus = useRefreshBranchStatus(); const permalink = getPathUrlAsString( getComponentSecurityHotspotsUrl(component.key, { @@ -67,7 +67,7 @@ export function HotspotHeader(props: HotspotHeaderProps) { const categoryStandard = standards?.[SecurityStandard.SONARSOURCE][rule.securityCategory]?.title; const handleStatusChange = async (statusOption: HotspotStatusOption) => { await props.onUpdateHotspot(true, statusOption); - refrechBranchStatus(); + refreshBranchStatus(); }; return ( diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx index 7594a22dfaf..216487db516 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx @@ -81,7 +81,7 @@ export default function HotspotViewerTabs(props: Props) { branchLike, } = props; - const refrechBranchStatus = useRefreshBranchStatus(); + const refreshBranchStatus = useRefreshBranchStatus(); const isSticky = useStickyDetection('.hotspot-tabs', { offset: TABS_OFFSET, }); @@ -163,7 +163,7 @@ export default function HotspotViewerTabs(props: Props) { const handleStatusChange = async (statusOption: HotspotStatusOption) => { await props.onUpdateHotspot(true, statusOption); - refrechBranchStatus(); + refreshBranchStatus(); }; React.useEffect(() => { diff --git a/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts index ca8d93eaa8d..6db7f0bb4a0 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts @@ -95,6 +95,8 @@ describe('openHotspot', () => { describe('openIssue', () => { it('should send the correct request to the IDE to open an issue', async () => { + let branchName: string | undefined = undefined; + const issueKey = 'my-issue-key'; const resp = new Response(); window.fetch = jest.fn((input: RequestInfo) => { @@ -103,7 +105,9 @@ describe('openIssue', () => { try { expect(calledUrl.searchParams.get('server')).toStrictEqual('http://localhost'); expect(calledUrl.searchParams.get('project')).toStrictEqual(PROJECT_KEY); - expect(calledUrl.searchParams.get('issue')).toStrictEqual('my-issue-key'); + expect(calledUrl.searchParams.get('issue')).toStrictEqual(issueKey); + // eslint-disable-next-line jest/no-conditional-in-test + expect(calledUrl.searchParams.get('branch') ?? undefined).toStrictEqual(branchName); } catch (error) { return Promise.reject(error); } @@ -111,7 +115,13 @@ describe('openIssue', () => { return Promise.resolve(resp); }); - const result = await openIssue(SONARLINT_PORT_START, PROJECT_KEY, 'my-issue-key'); + let result = await openIssue(SONARLINT_PORT_START, PROJECT_KEY, issueKey); + + expect(result).toBe(resp); + + branchName = 'branch-1'; + + result = await openIssue(SONARLINT_PORT_START, PROJECT_KEY, issueKey, branchName); expect(result).toBe(resp); }); diff --git a/server/sonar-web/src/main/js/helpers/sonarlint.ts b/server/sonar-web/src/main/js/helpers/sonarlint.ts index a9c378d491e..b4d9dbccd33 100644 --- a/server/sonar-web/src/main/js/helpers/sonarlint.ts +++ b/server/sonar-web/src/main/js/helpers/sonarlint.ts @@ -55,13 +55,22 @@ export function openHotspot(calledPort: number, projectKey: string, hotspotKey: return fetch(showUrl.toString()).then((response: Response) => checkStatus(response, true)); } -export function openIssue(calledPort: number, projectKey: string, issueKey: string) { +export function openIssue( + calledPort: number, + projectKey: string, + issueKey: string, + branchName?: string, +) { const showUrl = new URL(buildSonarLintEndpoint(calledPort, '/issues/show')); showUrl.searchParams.set('server', getHostUrl()); showUrl.searchParams.set('project', projectKey); showUrl.searchParams.set('issue', issueKey); + if (branchName !== undefined) { + showUrl.searchParams.set('branch', branchName); + } + return fetch(showUrl.toString()).then((response: Response) => checkStatus(response, true)); } diff --git a/server/sonar-web/src/main/js/queries/branch.tsx b/server/sonar-web/src/main/js/queries/branch.tsx index 4f3a95be6cf..78bba188984 100644 --- a/server/sonar-web/src/main/js/queries/branch.tsx +++ b/server/sonar-web/src/main/js/queries/branch.tsx @@ -128,9 +128,10 @@ export function useBranchesQuery(component?: Component, refetchInterval?: number : branchLikes.find( (b) => isBranch(b) && (prOrBranch === 'branch' ? b.name === name : b.isMain), ); + return { branchLikes, branchLike }; }, - // The check of the key must desapear once component state is in react-query + // The check of the key must disappear once component state is in react-query enabled: !!component && component.key === key[1], staleTime: refetchInterval ?? BRANCHES_STALE_TIME, refetchInterval, @@ -293,10 +294,10 @@ export function useSetMainBranchMutation() { } /** - * Helper functions that sould be avoid. Instead convert the component into functional + * Helper functions that sould be avoided. Instead convert the component into a functional one * and/or use proper react-query */ -const DELAY_REFRECH = 1_000; +const REFRESH_INTERVAL = 1_000; export function useRefreshBranchStatus(): () => void { const queryClient = useQueryClient(); @@ -311,7 +312,7 @@ export function useRefreshBranchStatus(): () => void { queryClient.invalidateQueries({ queryKey: invalidateDetailsKey, }); - }, DELAY_REFRECH), + }, REFRESH_INTERVAL), [invalidateDetailsKey, invalidateStatusKey], ); } |