Browse Source

SONAR-20447 Pass extra 'branch' parameter to the SonarLint call

tags/10.3.0.82913
David Cho-Lerat 7 months ago
parent
commit
8619068f5c

+ 3
- 2
server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx View File

@@ -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);

+ 2
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx View File

@@ -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');

+ 2
- 3
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx View File

@@ -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}

+ 30
- 18
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx View File

@@ -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));

+ 57
- 18
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx View File

@@ -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,
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx View File

@@ -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 (

+ 2
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx View File

@@ -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(() => {

+ 12
- 2
server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts View File

@@ -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);
});

+ 10
- 1
server/sonar-web/src/main/js/helpers/sonarlint.ts View File

@@ -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));
}


+ 5
- 4
server/sonar-web/src/main/js/queries/branch.tsx View File

@@ -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],
);
}

Loading…
Cancel
Save