Browse Source

SONAR-21467 Implement new branch overview header

tags/10.4.0.87286
stanislavh 4 months ago
parent
commit
3efe35084f
32 changed files with 443 additions and 682 deletions
  1. 27
    1
      server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts
  2. 2
    2
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  3. 4
    0
      server/sonar-web/src/main/js/app/components/componentContext/withComponentContext.tsx
  4. 2
    18
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  5. 0
    73
      server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
  6. 0
    23
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  7. 0
    119
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
  8. 10
    15
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx
  9. 77
    0
      server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx
  10. 63
    53
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  11. 0
    7
      server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx
  12. 6
    1
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
  13. 7
    7
      server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx
  14. 5
    5
      server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx
  15. 5
    5
      server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx
  16. 12
    11
      server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx
  17. 7
    7
      server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx
  18. 0
    138
      server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx
  19. 5
    5
      server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx
  20. 5
    5
      server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx
  21. 107
    0
      server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx
  22. 7
    7
      server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx
  23. 0
    113
      server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx
  24. 1
    1
      server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts
  25. 23
    22
      server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx
  26. 5
    2
      server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
  27. 40
    33
      server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
  28. 2
    1
      server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx
  29. 4
    0
      server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
  30. 14
    8
      server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
  31. 2
    0
      server/sonar-web/src/main/js/types/component.ts
  32. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 27
- 1
server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts View File

@@ -23,11 +23,18 @@ import { PAGE_SIZE } from '../../apps/background-tasks/constants';
import { parseDate } from '../../helpers/dates';
import { mockTask } from '../../helpers/mocks/tasks';
import { isDefined } from '../../helpers/types';
import { ActivityRequestParameters, Task, TaskStatuses, TaskTypes } from '../../types/tasks';
import {
ActivityRequestParameters,
Task,
TaskStatuses,
TaskTypes,
TaskWarning,
} from '../../types/tasks';
import {
cancelAllTasks,
cancelTask,
getActivity,
getAnalysisStatus,
getStatus,
getTask,
getTasksForComponent,
@@ -63,6 +70,7 @@ jest.mock('../ce');

export default class ComputeEngineServiceMock {
tasks: Task[];
taskWarnings: TaskWarning[] = [];
workers = { ...DEFAULT_WORKERS };

constructor() {
@@ -75,6 +83,7 @@ export default class ComputeEngineServiceMock {
jest.mocked(getWorkers).mockImplementation(this.handleGetWorkers);
jest.mocked(setWorkerCount).mockImplementation(this.handleSetWorkerCount);
jest.mocked(getTasksForComponent).mockImplementation(this.handleGetTaskForComponent);
jest.mocked(getAnalysisStatus).mockImplementation(this.handleAnalysisStatus);

this.tasks = cloneDeep(DEFAULT_TASKS);
}
@@ -89,6 +98,22 @@ export default class ComputeEngineServiceMock {
return Promise.resolve();
};

setTaskWarnings = (taskWarnings: TaskWarning[] = []) => {
this.taskWarnings = taskWarnings;
};

handleAnalysisStatus = (data: { component: string; branch?: string; pullRequest?: string }) => {
return Promise.resolve({
component: {
key: data.component,
name: data.component,
branch: data.branch,
pullRequest: data.pullRequest,
warnings: this.taskWarnings,
},
});
};

handleCancelTask = (id: string) => {
const task = this.tasks.find((t) => t.id === id);

@@ -217,6 +242,7 @@ export default class ComputeEngineServiceMock {

reset() {
this.tasks = cloneDeep(DEFAULT_TASKS);
this.taskWarnings = [];
this.workers = { ...DEFAULT_WORKERS };
}


+ 2
- 2
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -258,12 +258,13 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
const componentProviderProps = React.useMemo(
() => ({
component,
currentTask,
isInProgress,
isPending,
onComponentChange: handleComponentChange,
fetchComponent,
}),
[component, isInProgress, isPending, handleComponentChange, fetchComponent],
[component, currentTask, isInProgress, isPending, handleComponentChange, fetchComponent],
);

// Show not found component when, after loading:
@@ -289,7 +290,6 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
createPortal(
<ComponentNav
component={component}
currentTask={currentTask}
isInProgress={isInProgress}
isPending={isPending}
projectBindingErrors={projectBindingErrors}

+ 4
- 0
server/sonar-web/src/main/js/app/components/componentContext/withComponentContext.tsx View File

@@ -39,3 +39,7 @@ export default function withComponentContext<P extends Partial<ComponentContextS
}
};
}

export function useComponent() {
return React.useContext(ComponentContext);
}

+ 2
- 18
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

@@ -26,7 +26,6 @@ import { ProjectAlmBindingConfigurationErrors } from '../../../../types/alm-sett
import { Branch } from '../../../../types/branch-like';
import { ComponentQualifier } from '../../../../types/component';
import { Feature } from '../../../../types/features';
import { Task } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import RecentHistory from '../../RecentHistory';
import withAvailableFeatures, {
@@ -34,28 +33,19 @@ import withAvailableFeatures, {
} from '../../available-features/withAvailableFeatures';
import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif';
import Header from './Header';
import HeaderMeta from './HeaderMeta';
import Menu from './Menu';

export interface ComponentNavProps extends WithAvailableFeaturesProps {
branchLike?: Branch;
component: Component;
currentTask?: Task;
isInProgress?: boolean;
isPending?: boolean;
projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
}

function ComponentNav(props: Readonly<ComponentNavProps>) {
const {
branchLike,
component,
currentTask,
hasFeature,
isInProgress,
isPending,
projectBindingErrors,
} = props;
const { branchLike, component, hasFeature, isInProgress, isPending, projectBindingErrors } =
props;

React.useEffect(() => {
const { breadcrumbs, key, name } = component;
@@ -76,12 +66,6 @@ function ComponentNav(props: Readonly<ComponentNavProps>) {
<TopBar id="context-navigation" aria-label={translate('qualifier', component.qualifier)}>
<div className="sw-min-h-10 sw-flex sw-justify-between">
<Header component={component} />
<HeaderMeta
component={component}
currentTask={currentTask}
isInProgress={isInProgress}
isPending={isPending}
/>
</div>
<Menu component={component} isInProgress={isInProgress} isPending={isPending} />
</TopBar>

+ 0
- 73
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx View File

@@ -1,73 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { TextMuted } from 'design-system';
import * as React from 'react';
import HomePageSelect from '../../../../components/controls/HomePageSelect';
import { isBranch, isPullRequest } from '../../../../helpers/branch-like';
import { translateWithParameters } from '../../../../helpers/l10n';
import { useBranchesQuery } from '../../../../queries/branch';
import { Task } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../../types/users';
import withCurrentUserContext from '../../current-user/withCurrentUserContext';
import { AnalysisStatus } from './AnalysisStatus';
import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation';
import { getCurrentPage } from './utils';

export interface HeaderMetaProps {
component: Component;
currentUser: CurrentUser;
currentTask?: Task;
isInProgress?: boolean;
isPending?: boolean;
}

export function HeaderMeta(props: HeaderMetaProps) {
const { component, currentUser, currentTask, isInProgress, isPending } = props;

const { data: { branchLike } = {} } = useBranchesQuery(component);

const isABranch = isBranch(branchLike);

const currentPage = getCurrentPage(component, branchLike);

return (
<div className="sw-flex sw-items-center sw-flex-shrink sw-min-w-0">
<AnalysisStatus
component={component}
currentTask={currentTask}
isInProgress={isInProgress}
isPending={isPending}
/>
{branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />}
{component.version !== undefined && isABranch && (
<TextMuted
text={translateWithParameters('version_x', component.version)}
className="sw-ml-4 sw-whitespace-nowrap"
/>
)}
{isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && (
<HomePageSelect className="sw-ml-2" currentPage={currentPage} />
)}
</div>
);
}

export default withCurrentUserContext(HeaderMeta);

+ 0
- 23
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx View File

@@ -21,33 +21,10 @@ import { screen } from '@testing-library/react';
import React from 'react';
import { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { mockTask } from '../../../../../helpers/mocks/tasks';
import { renderApp } from '../../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../../types/component';
import { TaskStatuses } from '../../../../../types/tasks';
import ComponentNav, { ComponentNavProps } from '../ComponentNav';

it('renders correctly when there is a background task in progress', () => {
renderComponentNav({ isInProgress: true });
expect(
screen.getByText('project_navigation.analysis_status.in_progress', { exact: false }),
).toBeInTheDocument();
});

it('renders correctly when there is a background task pending', () => {
renderComponentNav({ isPending: true });
expect(
screen.getByText('project_navigation.analysis_status.pending', { exact: false }),
).toBeInTheDocument();
});

it('renders correctly when there is a failing background task', () => {
renderComponentNav({ currentTask: mockTask({ status: TaskStatuses.Failed }) });
expect(
screen.getByText('project_navigation.analysis_status.failed', { exact: false }),
).toBeInTheDocument();
});

it('renders correctly when the project binding is incorrect', () => {
renderComponentNav({
projectBindingErrors: mockProjectAlmBindingConfigurationErrors(),

+ 0
- 119
server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx View File

@@ -1,119 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { getAnalysisStatus } from '../../../../../api/ce';
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { mockTask } from '../../../../../helpers/mocks/tasks';
import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
import { renderApp } from '../../../../../helpers/testReactTestingUtils';
import { Feature } from '../../../../../types/features';
import { TaskStatuses } from '../../../../../types/tasks';
import { CurrentUser } from '../../../../../types/users';
import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta';

jest.mock('../../../../../api/ce');

const handler = new BranchesServiceMock();

beforeEach(() => handler.reset());

it('should render correctly for a branch with warnings', async () => {
const user = userEvent.setup();
jest.mocked(getAnalysisStatus).mockResolvedValue({
component: {
warnings: [{ dismissable: false, key: 'key', message: 'bar' }],
key: 'compkey',
name: 'me',
},
});
renderHeaderMeta({}, undefined, 'branch=normal-branch&id=my-project');

expect(await screen.findByText('version_x.0.0.1')).toBeInTheDocument();

expect(
await screen.findByText('project_navigation.analysis_status.warnings'),
).toBeInTheDocument();

await user.click(screen.getByText('project_navigation.analysis_status.details_link'));

expect(screen.getByRole('heading', { name: 'warnings' })).toBeInTheDocument();
});

it('should handle a branch with missing version and no warnings', () => {
jest.mocked(getAnalysisStatus).mockResolvedValue({
component: {
warnings: [],
key: 'compkey',
name: 'me',
},
});
renderHeaderMeta({ component: mockComponent({ version: undefined }) });

expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument();
});

it('should render correctly with a failed analysis', async () => {
const user = userEvent.setup();

renderHeaderMeta({
currentTask: mockTask({
status: TaskStatuses.Failed,
errorMessage: 'this is the error message',
}),
});

expect(await screen.findByText('project_navigation.analysis_status.failed')).toBeInTheDocument();

await user.click(screen.getByText('project_navigation.analysis_status.details_link'));

expect(screen.getByRole('heading', { name: 'error' })).toBeInTheDocument();
});

it('should render correctly for a pull request', async () => {
renderHeaderMeta({}, undefined, 'pullRequest=01&id=my-project');

expect(
await screen.findByText('branch_like_navigation.for_merge_into_x_from_y'),
).toBeInTheDocument();
expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
});

it('should render correctly when the user is not logged in', () => {
renderHeaderMeta({}, mockCurrentUser({ dismissedNotices: {} }));
expect(screen.queryByText('homepage.current.is_default')).not.toBeInTheDocument();
expect(screen.queryByText('homepage.current')).not.toBeInTheDocument();
expect(screen.queryByText('homepage.check')).not.toBeInTheDocument();
});

function renderHeaderMeta(
props: Partial<HeaderMetaProps> = {},
currentUser: CurrentUser = mockLoggedInUser(),
params?: string,
) {
return renderApp('/', <HeaderMeta component={mockComponent({ version: '0.0.1' })} {...props} />, {
currentUser,
navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project',
featureList: [Feature.BranchSupport],
});
}

+ 10
- 15
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx View File

@@ -19,36 +19,31 @@
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { isPullRequest } from '../../../../../helpers/branch-like';
import { translate, translateWithParameters } from '../../../../../helpers/l10n';
import { BranchLike } from '../../../../../types/branch-like';
import { PullRequest } from '../../../../../types/branch-like';

export interface CurrentBranchLikeMergeInformationProps {
currentBranchLike: BranchLike;
pullRequest: PullRequest;
}

export function CurrentBranchLikeMergeInformation(props: CurrentBranchLikeMergeInformationProps) {
const { currentBranchLike } = props;

if (!isPullRequest(currentBranchLike)) {
return null;
}

export function CurrentBranchLikeMergeInformation({
pullRequest,
}: Readonly<CurrentBranchLikeMergeInformationProps>) {
return (
<span
className="sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-mx-1 sw-flex-shrink sw-min-w-0"
className="sw-w-[400px] sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-flex-shrink sw-min-w-0"
title={translateWithParameters(
'branch_like_navigation.for_merge_into_x_from_y.title',
currentBranchLike.target,
currentBranchLike.branch,
pullRequest.target,
pullRequest.branch,
)}
>
<FormattedMessage
defaultMessage={translate('branch_like_navigation.for_merge_into_x_from_y')}
id="branch_like_navigation.for_merge_into_x_from_y"
values={{
target: <strong>{currentBranchLike.target}</strong>,
branch: <strong>{currentBranchLike.branch}</strong>,
target: <strong>{pullRequest.target}</strong>,
branch: <strong>{pullRequest.branch}</strong>,
}}
/>
</span>

+ 77
- 0
server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx View File

@@ -0,0 +1,77 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { SeparatorCircleIcon } from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import { getCurrentPage } from '../../../app/components/nav/component/utils';
import ComponentReportActions from '../../../components/controls/ComponentReportActions';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import { findMeasure, formatMeasure } from '../../../helpers/measures';
import { Branch } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import { HomePage } from '../../../types/users';

interface Props {
component: Component;
branch: Branch;
measures: MeasureEnhanced[];
}

export default function BranchMetaTopBar({ branch, measures, component }: Readonly<Props>) {
const intl = useIntl();

const currentPage = getCurrentPage(component, branch) as HomePage;
const locMeasure = findMeasure(measures, MetricKey.lines);

const leftSection = (
<h1 className="sw-flex sw-gap-2 sw-items-center sw-heading-md">{branch.name}</h1>
);
const rightSection = (
<div className="sw-flex sw-gap-2 sw-items-center">
{locMeasure && (
<>
<div className="sw-flex sw-items-center sw-gap-1">
<strong>{formatMeasure(locMeasure.value, MetricType.ShortInteger)}</strong>
{intl.formatMessage({ id: 'metric.ncloc.name' })}
</div>
<SeparatorCircleIcon />
</>
)}
{component.version && (
<>
<div className="sw-flex sw-items-center sw-gap-1">
{intl.formatMessage({ id: 'version_x' }, { '0': <strong>{component.version}</strong> })}
</div>
<SeparatorCircleIcon />
</>
)}
<HomePageSelect currentPage={currentPage} type="button" />
<ComponentReportActions component={component} branch={branch} />
</div>
);

return (
<div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm sw-mb-2">
{leftSection}
{rightSection}
</div>
);
}

+ 63
- 53
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

@@ -17,7 +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 { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
import { BasicSeparator, LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
import * as React from 'react';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import { useLocation } from '../../../components/hoc/withRouter';
@@ -30,9 +30,11 @@ import { ComponentQualifier } from '../../../types/component';
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
import { QualityGateStatus } from '../../../types/quality-gates';
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
import { AnalysisStatus } from '../components/AnalysisStatus';
import { MeasuresTabs } from '../utils';
import AcceptedIssuesPanel from './AcceptedIssuesPanel';
import ActivityPanel from './ActivityPanel';
import BranchMetaTopBar from './BranchMetaTopBar';
import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
import MeasuresPanel from './MeasuresPanel';
import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
@@ -115,66 +117,74 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
{projectIsEmpty ? (
<NoCodeWarning branchLike={branch} component={component} measures={measures} />
) : (
<div className="sw-flex">
<div className="sw-w-1/3 sw-mr-12 sw-pt-6">
<QualityGatePanel
component={component}
loading={loadingStatus}
qgStatuses={qgStatuses}
qualityGate={qualityGate}
/>
</div>

<div className="sw-flex-1">
<div className="sw-flex sw-flex-col sw-pt-6">
<TabsPanel
analyses={analyses}
appLeak={appLeak}
branch={branch}
<div>
{branch && (
<>
<BranchMetaTopBar branch={branch} component={component} measures={measures} />
<BasicSeparator />
</>
)}
<AnalysisStatus className="sw-mt-6" component={component} />
<div className="sw-flex">
<div className="sw-w-1/3 sw-mr-12 sw-pt-6">
<QualityGatePanel
component={component}
loading={loadingStatus}
period={period}
qgStatuses={qgStatuses}
isNewCode={isNewCodeTab}
onTabSelect={selectTab}
>
{!hasNewCodeMeasures && isNewCodeTab ? (
<MeasuresPanelNoNewCode
branch={branch}
component={component}
period={period}
/>
) : (
<>
<MeasuresPanel
branch={branch}
component={component}
measures={measures}
isNewCode={isNewCodeTab}
/>
qualityGate={qualityGate}
/>
</div>

<AcceptedIssuesPanel
<div className="sw-flex-1">
<div className="sw-flex sw-flex-col sw-pt-6">
<TabsPanel
analyses={analyses}
appLeak={appLeak}
component={component}
loading={loadingStatus}
period={period}
qgStatuses={qgStatuses}
isNewCode={isNewCodeTab}
onTabSelect={selectTab}
>
{!hasNewCodeMeasures && isNewCodeTab ? (
<MeasuresPanelNoNewCode
branch={branch}
component={component}
measures={measures}
isNewCode={isNewCodeTab}
loading={loadingStatus}
period={period}
/>
</>
)}
</TabsPanel>
) : (
<>
<MeasuresPanel
branch={branch}
component={component}
measures={measures}
isNewCode={isNewCodeTab}
/>

<ActivityPanel
analyses={analyses}
branchLike={branch}
component={component}
graph={graph}
leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)}
loading={loadingHistory}
measuresHistory={measuresHistory}
metrics={metrics}
onGraphChange={onGraphChange}
/>
<AcceptedIssuesPanel
branch={branch}
component={component}
measures={measures}
isNewCode={isNewCodeTab}
loading={loadingStatus}
/>
</>
)}
</TabsPanel>

<ActivityPanel
analyses={analyses}
branchLike={branch}
component={component}
graph={graph}
leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)}
loading={loadingHistory}
measuresHistory={measuresHistory}
metrics={metrics}
onGraphChange={onGraphChange}
/>
</div>
</div>
</div>
</div>

+ 0
- 7
server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx View File

@@ -29,11 +29,9 @@ import {
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import DocLink from '../../../components/common/DocLink';
import ComponentReportActions from '../../../components/controls/ComponentReportActions';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures';
import { ApplicationPeriod } from '../../../types/application';
import { Branch } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity';
import { QualityGateStatus } from '../../../types/quality-gates';
@@ -45,7 +43,6 @@ import { LeakPeriodInfo } from './LeakPeriodInfo';
export interface MeasuresPanelProps {
analyses?: Analysis[];
appLeak?: ApplicationPeriod;
branch?: Branch;
component: Component;
loading?: boolean;
period?: Period;
@@ -60,7 +57,6 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {
const {
analyses,
appLeak,
branch,
component,
loading,
period,
@@ -128,9 +124,6 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {

return (
<div data-test="overview__measures-panel">
<div className="sw-float-right -sw-mt-6">
<ComponentReportActions component={component} branch={branch} />
</div>
<div className="sw-flex sw-mb-4">
<PageTitle as="h2" text={translate('overview.measures')} />
</div>

+ 6
- 1
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx View File

@@ -210,8 +210,12 @@ describe('project overview', () => {
);
renderBranchOverview();

// Meta info
expect(await screen.findByText('master')).toBeInTheDocument();
expect(screen.getByText('version-1.0')).toBeInTheDocument();

// QG panel
expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
expect(screen.getByText('metric.level.OK')).toBeInTheDocument();
expect(screen.getByText('overview.passed.clean_code')).toBeInTheDocument();
expect(
screen.queryByText('overview.quality_gate.conditions.cayc.warning'),
@@ -540,6 +544,7 @@ function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
breadcrumbs: [mockComponent({ key: 'foo' })],
key: 'foo',
name: 'Foo',
version: 'version-1.0',
})}
{...props}
/>

server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx → server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx View File

@@ -21,13 +21,13 @@ import { Link } from 'design-system';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useLocation } from 'react-router-dom';
import { isBranch, isMainBranch, isPullRequest } from '../../../../helpers/branch-like';
import { hasMessage, translate } from '../../../../helpers/l10n';
import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls';
import { useBranchesQuery } from '../../../../queries/branch';
import { BranchLike } from '../../../../types/branch-like';
import { Task } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import { isBranch, isMainBranch, isPullRequest } from '../../../helpers/branch-like';
import { hasMessage, translate } from '../../../helpers/l10n';
import { getComponentBackgroundTaskUrl } from '../../../helpers/urls';
import { useBranchesQuery } from '../../../queries/branch';
import { BranchLike } from '../../../types/branch-like';
import { Task } from '../../../types/tasks';
import { Component } from '../../../types/types';

interface Props {
component: Component;

server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx → server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx View File

@@ -18,11 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Modal from '../../../../components/controls/Modal';
import { ResetButtonLink } from '../../../../components/controls/buttons';
import { hasMessage, translate } from '../../../../helpers/l10n';
import { Task } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import Modal from '../../../components/controls/Modal';
import { ResetButtonLink } from '../../../components/controls/buttons';
import { hasMessage, translate } from '../../../helpers/l10n';
import { Task } from '../../../types/tasks';
import { Component } from '../../../types/types';
import { AnalysisErrorMessage } from './AnalysisErrorMessage';
import { AnalysisLicenseError } from './AnalysisLicenseError';


server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx → server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx View File

@@ -18,11 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Link from '../../../../components/common/Link';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { ComponentQualifier } from '../../../../types/component';
import { Task } from '../../../../types/tasks';
import { AppStateContext } from '../../app-state/AppStateContext';
import { AppStateContext } from '../../../app/components/app-state/AppStateContext';
import Link from '../../../components/common/Link';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { ComponentQualifier } from '../../../types/component';
import { Task } from '../../../types/tasks';
import { useLicenseIsValid } from './useLicenseIsValid';

interface Props {

server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx → server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx View File

@@ -17,24 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import { FlagMessage, Link, Spinner } from 'design-system';
import * as React from 'react';
import { translate } from '../../../../helpers/l10n';
import { useBranchWarningQuery } from '../../../../queries/branch';
import { Task, TaskStatuses } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import { useComponent } from '../../../app/components/componentContext/withComponentContext';
import { translate } from '../../../helpers/l10n';
import { useBranchWarningQuery } from '../../../queries/branch';
import { TaskStatuses } from '../../../types/tasks';
import { Component } from '../../../types/types';
import { AnalysisErrorModal } from './AnalysisErrorModal';
import AnalysisWarningsModal from './AnalysisWarningsModal';

export interface HeaderMetaProps {
currentTask?: Task;
component: Component;
isInProgress?: boolean;
isPending?: boolean;
className?: string;
}

export function AnalysisStatus(props: HeaderMetaProps) {
const { component, currentTask, isInProgress, isPending } = props;
const { className, component } = props;
const { currentTask, isPending, isInProgress } = useComponent();
const { data: warnings, isLoading } = useBranchWarningQuery(component);

const [modalIsVisible, setDisplayModal] = React.useState(false);
@@ -47,7 +48,7 @@ export function AnalysisStatus(props: HeaderMetaProps) {

if (isInProgress || isPending) {
return (
<div className="sw-flex sw-items-center">
<div data-test="analysis-status" className={classNames('sw-flex sw-items-center', className)}>
<Spinner />
<span className="sw-ml-1">
{isInProgress
@@ -61,7 +62,7 @@ export function AnalysisStatus(props: HeaderMetaProps) {
if (currentTask?.status === TaskStatuses.Failed) {
return (
<>
<FlagMessage variant="error">
<FlagMessage data-test="analysis-status" variant="error" className={className}>
<span>{translate('project_navigation.analysis_status.failed')}</span>
<Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
{translate('project_navigation.analysis_status.details_link')}
@@ -81,7 +82,7 @@ export function AnalysisStatus(props: HeaderMetaProps) {
if (!isLoading && warnings && warnings.length > 0) {
return (
<>
<FlagMessage variant="warning">
<FlagMessage data-test="analysis-status" variant="warning" className={className}>
<span>{translate('project_navigation.analysis_status.warnings')}</span>
<Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
{translate('project_navigation.analysis_status.details_link')}

server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx → server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx View File

@@ -19,13 +19,13 @@
*/
import { DangerButtonSecondary, FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system';
import * as React from 'react';
import { translate } from '../../../../helpers/l10n';
import { sanitizeStringRestricted } from '../../../../helpers/sanitize';
import { useDismissBranchWarningMutation } from '../../../../queries/branch';
import { TaskWarning } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import { CurrentUser } from '../../../../types/users';
import withCurrentUserContext from '../../current-user/withCurrentUserContext';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import { translate } from '../../../helpers/l10n';
import { sanitizeStringRestricted } from '../../../helpers/sanitize';
import { useDismissBranchWarningMutation } from '../../../queries/branch';
import { TaskWarning } from '../../../types/tasks';
import { Component } from '../../../types/types';
import { CurrentUser } from '../../../types/users';

interface Props {
component: Component;

+ 0
- 138
server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx View File

@@ -1,138 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { FormattedMessage } from 'react-intl';
import { getLeakValue } from '../../../components/measure/utils';
import DrilldownLink from '../../../components/shared/DrilldownLink';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { Component, MeasureEnhanced } from '../../../types/types';
import {
getMeasurementIconClass,
getMeasurementLabelKeys,
getMeasurementLinesMetricKey,
getMeasurementMetricKey,
MeasurementType,
} from '../utils';

interface Props {
branchLike?: BranchLike;
centered?: boolean;
component: Component;
measures: MeasureEnhanced[];
type: MeasurementType;
useDiffMetric?: boolean;
}

export default class MeasurementLabel extends React.Component<Props> {
getLabelText = () => {
const { branchLike, component, measures, type, useDiffMetric = false } = this.props;
const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric);
const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric);
const measure = findMeasure(measures, linesMetric);

if (!measure) {
return translate(labelKey);
}

const value = useDiffMetric ? getLeakValue(measure) : measure.value;

return (
<FormattedMessage
defaultMessage={translate(expandedLabelKey)}
id={expandedLabelKey}
values={{
count: (
<DrilldownLink
branchLike={branchLike}
className="big"
component={component.key}
metric={linesMetric}
>
{formatMeasure(value, 'SHORT_INT')}
</DrilldownLink>
),
}}
/>
);
};

render() {
const { branchLike, centered, component, measures, type, useDiffMetric = false } = this.props;
const iconClass = getMeasurementIconClass(type);
const metricKey = getMeasurementMetricKey(type, useDiffMetric);
const measure = findMeasure(measures, metricKey);

let value;
if (measure) {
value = useDiffMetric ? getLeakValue(measure) : measure.value;
}

if (value === undefined) {
return (
<div className="display-flex-center">
<span aria-label={translate('no_data')} className="overview-measures-empty-value" />
<span className="big-spacer-left">{this.getLabelText()}</span>
</div>
);
}

const icon = React.createElement(iconClass, { size: 'big', value: Number(value) });
const formattedValue = formatMeasure(value, 'PERCENT', {
decimals: 2,
omitExtraDecimalZeros: true,
});
const link = (
<DrilldownLink
ariaLabel={translateWithParameters(
'overview.see_more_details_on_x_of_y',
formattedValue,
localizeMetric(metricKey),
)}
branchLike={branchLike}
className="overview-measures-value text-light"
component={component.key}
metric={metricKey}
>
{formattedValue}
</DrilldownLink>
);
const label = this.getLabelText();

return centered ? (
<div className="display-flex-column flex-1">
<div className="display-flex-center display-flex-justify-center">
<span className="big-spacer-right">{icon}</span>
{link}
</div>
<div className="spacer-top text-center">{label}</div>
</div>
) : (
<div className="display-flex-center">
<span className="big-spacer-right">{icon}</span>
<div className="display-flex-column">
<span>{link}</span>
<span className="spacer-top">{label}</span>
</div>
</div>
);
}
}

server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx → server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx View File

@@ -19,11 +19,11 @@
*/
import { screen } from '@testing-library/react';
import * as React from 'react';
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { mockTask } from '../../../../../helpers/mocks/tasks';
import { renderApp } from '../../../../../helpers/testReactTestingUtils';
import { Feature } from '../../../../../types/features';
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockTask } from '../../../../helpers/mocks/tasks';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { Feature } from '../../../../types/features';
import { AnalysisErrorMessage } from '../AnalysisErrorMessage';

const handler = new BranchesServiceMock();

server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx → server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx View File

@@ -19,13 +19,13 @@
*/
import { screen } from '@testing-library/react';
import * as React from 'react';
import { isValidLicense } from '../../../../../api/editions';
import { mockTask } from '../../../../../helpers/mocks/tasks';
import { mockAppState } from '../../../../../helpers/testMocks';
import { renderApp } from '../../../../../helpers/testReactTestingUtils';
import { isValidLicense } from '../../../../api/editions';
import { mockTask } from '../../../../helpers/mocks/tasks';
import { mockAppState } from '../../../../helpers/testMocks';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { AnalysisLicenseError } from '../AnalysisLicenseError';

jest.mock('../../../../../api/editions', () => ({
jest.mock('../../../../api/editions', () => ({
isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }),
}));


+ 107
- 0
server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx View File

@@ -0,0 +1,107 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock';
import { useComponent } from '../../../../app/components/componentContext/withComponentContext';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockTask, mockTaskWarning } from '../../../../helpers/mocks/tasks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { TaskStatuses } from '../../../../types/tasks';
import { AnalysisStatus } from '../AnalysisStatus';

const branchesHandler = new BranchesServiceMock();
const handler = new ComputeEngineServiceMock();

jest.mock('../../../../app/components/componentContext/withComponentContext', () => ({
useComponent: jest.fn(() => ({
isInProgress: true,
isPending: false,
currentTask: mockTask(),
component: mockComponent(),
})),
}));

beforeEach(() => {
branchesHandler.reset();
handler.reset();
});

it('renders correctly when there is a background task in progress', () => {
renderAnalysisStatus();
expect(
screen.getByText('project_navigation.analysis_status.in_progress', { exact: false }),
).toBeInTheDocument();
});

it('renders correctly when there is a background task pending', () => {
jest.mocked(useComponent).mockReturnValue({
isInProgress: false,
isPending: true,
currentTask: mockTask(),
onComponentChange: jest.fn(),
fetchComponent: jest.fn(),
});
renderAnalysisStatus();
expect(
screen.getByText('project_navigation.analysis_status.pending', { exact: false }),
).toBeInTheDocument();
});

it('renders correctly when there is a failing background task', () => {
jest.mocked(useComponent).mockReturnValue({
isInProgress: false,
isPending: false,
currentTask: mockTask({ status: TaskStatuses.Failed }),
onComponentChange: jest.fn(),
fetchComponent: jest.fn(),
});
renderAnalysisStatus();
expect(
screen.getByText('project_navigation.analysis_status.failed', { exact: false }),
).toBeInTheDocument();
});

it('renders correctly when there are analysis warnings', async () => {
const user = userEvent.setup();
jest.mocked(useComponent).mockReturnValue({
isInProgress: false,
isPending: false,
currentTask: mockTask(),
onComponentChange: jest.fn(),
fetchComponent: jest.fn(),
});
handler.setTaskWarnings([mockTaskWarning({ message: 'warning 1' })]);
renderAnalysisStatus();

await user.click(await screen.findByText('project_navigation.analysis_status.details_link'));
expect(screen.getByText('warning 1')).toBeInTheDocument();
await user.click(screen.getByText('close'));
expect(screen.queryByText('warning 1')).not.toBeInTheDocument();
});

function renderAnalysisStatus(overrides: Partial<Parameters<typeof AnalysisStatus>[0]> = {}) {
return renderComponent(
<AnalysisStatus component={mockComponent()} {...overrides} />,
'/?id=my-project',
);
}

server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx → server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx View File

@@ -19,14 +19,14 @@
*/
import { screen } from '@testing-library/react';
import * as React from 'react';
import { AnalysisWarningsModal } from '../../../app/components/nav/component/AnalysisWarningsModal';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockTaskWarning } from '../../../helpers/mocks/tasks';
import { mockCurrentUser } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { ComponentPropsType } from '../../../helpers/testUtils';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockTaskWarning } from '../../../../helpers/mocks/tasks';
import { mockCurrentUser } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { ComponentPropsType } from '../../../../helpers/testUtils';
import { AnalysisWarningsModal } from '../AnalysisWarningsModal';

jest.mock('../../../api/ce', () => ({
jest.mock('../../../../api/ce', () => ({
dismissAnalysisWarning: jest.fn().mockResolvedValue(null),
getTask: jest.fn().mockResolvedValue({
warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n third line'],

+ 0
- 113
server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx View File

@@ -1,113 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
import * as React from 'react';
import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { MetricKey } from '../../../../types/metrics';
import { MeasurementType } from '../../utils';
import MeasurementLabel from '../MeasurementLabel';

it('should render correctly for coverage', async () => {
renderMeasurementLabel();
expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument();

renderMeasurementLabel({
measures: [
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) }),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.lines_to_cover }) }),
],
});
expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument();
expect(await screen.findByText('overview.coverage_on_X_lines')).toBeInTheDocument();

renderMeasurementLabel({
measures: [
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_coverage }) }),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_lines_to_cover }) }),
],
useDiffMetric: true,
});
expect(screen.getByRole('link', { name: /.*new_coverage.*/ })).toBeInTheDocument();
expect(await screen.findByText('overview.coverage_on_X_lines')).toBeInTheDocument();
expect(await screen.findByText('overview.coverage_on_X_new_lines')).toBeInTheDocument();
});

it('should render correctly for duplications', async () => {
renderMeasurementLabel({
measures: [
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.duplicated_lines_density }) }),
],
type: MeasurementType.Duplication,
});
expect(
screen.getByRole('link', {
name: 'overview.see_more_details_on_x_of_y.1.0%.metric.duplicated_lines_density.name',
}),
).toBeInTheDocument();
expect(await screen.findByText('metric.duplicated_lines_density.short_name')).toBeInTheDocument();

renderMeasurementLabel({
measures: [
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.duplicated_lines_density }) }),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.ncloc }) }),
],
type: MeasurementType.Duplication,
});
expect(await screen.findByText('metric.duplicated_lines_density.short_name')).toBeInTheDocument();
expect(await screen.findByText('overview.duplications_on_X_lines')).toBeInTheDocument();

renderMeasurementLabel({
measures: [
mockMeasureEnhanced({
metric: mockMetric({ key: MetricKey.new_duplicated_lines_density }),
}),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_lines }) }),
],
type: MeasurementType.Duplication,
useDiffMetric: true,
});

expect(
screen.getByRole('link', {
name: 'overview.see_more_details_on_x_of_y.1.0%.metric.new_duplicated_lines_density.name',
}),
).toBeInTheDocument();
expect(await screen.findByText('overview.duplications_on_X_new_lines')).toBeInTheDocument();
});

it('should render correctly with no value', async () => {
renderMeasurementLabel({ measures: [] });
expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument();
});

function renderMeasurementLabel(props: Partial<MeasurementLabel['props']> = {}) {
return renderComponent(
<MeasurementLabel
branchLike={mockPullRequest()}
component={mockComponent()}
measures={[mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) })]}
type={MeasurementType.Coverage}
{...props}
/>,
);
}

server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts → server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React, { useEffect } from 'react';
import { isValidLicense } from '../../../../api/editions';
import { isValidLicense } from '../../../api/editions';

export function useLicenseIsValid(): [boolean, boolean] {
const [licenseIsValid, setLicenseIsValid] = React.useState(false);

server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx → server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx View File

@@ -17,56 +17,57 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { SeparatorCircleIcon } from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import CurrentBranchLikeMergeInformation from '../../../app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation';
import DateFromNow from '../../../components/intl/DateFromNow';
import { getLeakValue } from '../../../components/measure/utils';
import { isPullRequest } from '../../../helpers/branch-like';
import { findMeasure, formatMeasure } from '../../../helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { PullRequest } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { MeasureEnhanced } from '../../../types/types';

interface Props {
branchLike: BranchLike;
pullRequest: PullRequest;
measures: MeasureEnhanced[];
}

export default function MetaTopBar({ branchLike, measures }: Readonly<Props>) {
export default function PullRequestMetaTopBar({ pullRequest, measures }: Readonly<Props>) {
const intl = useIntl();
const isPR = isPullRequest(branchLike);

const leftSection = (
<div>
{isPR ? (
<>
<strong className="sw-body-sm-highlight sw-mr-1">
{formatMeasure(
getLeakValue(findMeasure(measures, MetricKey.new_lines)),
MetricType.ShortInteger,
) ?? '0'}
</strong>
{intl.formatMessage({ id: 'metric.new_lines.name' })}
</>
) : null}
<strong className="sw-body-sm-highlight sw-mr-1">
{formatMeasure(
getLeakValue(findMeasure(measures, MetricKey.new_lines)),
MetricType.ShortInteger,
) || '0'}
</strong>
{intl.formatMessage({ id: 'metric.new_lines.name' })}
</div>
);
const rightSection = (
<div>
{branchLike.analysisDate
? intl.formatMessage(
<div className="sw-flex sw-items-center sw-gap-2">
<CurrentBranchLikeMergeInformation pullRequest={pullRequest} />

{pullRequest.analysisDate && (
<>
<SeparatorCircleIcon />
{intl.formatMessage(
{
id: 'overview.last_analysis_x',
},
{
date: (
<strong className="sw-body-sm-highlight">
<DateFromNow date={branchLike.analysisDate} />
<DateFromNow date={pullRequest.analysisDate} />
</strong>
),
},
)
: null}
)}
</>
)}
</div>
);


+ 5
- 2
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx View File

@@ -28,13 +28,14 @@ import { useComponentMeasuresWithMetricsQuery } from '../../../queries/component
import { useComponentQualityGateQuery } from '../../../queries/quality-gates';
import { PullRequest } from '../../../types/branch-like';
import { Component } from '../../../types/types';
import { AnalysisStatus } from '../components/AnalysisStatus';
import BranchQualityGate from '../components/BranchQualityGate';
import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
import MetaTopBar from '../components/MetaTopBar';
import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
import '../styles.css';
import { PR_METRICS, Status } from '../utils';
import MeasuresCardPanel from './MeasuresCardPanel';
import PullRequestMetaTopBar from './PullRequestMetaTopBar';
import SonarLintAd from './SonarLintAd';

interface Props {
@@ -97,9 +98,11 @@ export default function PullRequestOverview(props: Readonly<Readonly<Props>>) {
<CenteredLayout>
<PageContentFontWrapper className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12 sw-body-sm">
<div className="sw-col-start-2 sw-col-span-10">
<MetaTopBar branchLike={pullRequest} measures={measures} />
<PullRequestMetaTopBar pullRequest={pullRequest} measures={measures} />
<BasicSeparator className="sw-my-4" />

<AnalysisStatus className="sw-mb-4" component={component} />

{ignoredConditions && <IgnoredConditionWarning />}

{status && (

+ 40
- 33
server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx View File

@@ -17,12 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { DiscreetInteractiveIcon, HomeFillIcon, HomeIcon } from 'design-system';
import * as React from 'react';
import { ButtonSecondary, DiscreetInteractiveIcon, HomeFillIcon, HomeIcon } from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import { setHomePage } from '../../api/users';
import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
import { translate } from '../../helpers/l10n';
import { isSameHomePage } from '../../helpers/users';
import { HomePage, isLoggedIn } from '../../types/users';
import Tooltip from './Tooltip';
@@ -31,55 +31,62 @@ interface Props
extends Pick<CurrentUserContextInterface, 'currentUser' | 'updateCurrentUserHomepage'> {
className?: string;
currentPage: HomePage;
type?: 'button' | 'icon';
}

export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' };

export class HomePageSelect extends React.PureComponent<Props> {
async setCurrentUserHomepage(homepage: HomePage) {
const { currentUser } = this.props;
export function HomePageSelect(props: Readonly<Props>) {
const { currentPage, className, type = 'icon', currentUser, updateCurrentUserHomepage } = props;
const intl = useIntl();

if (!isLoggedIn(currentUser)) {
return null;
}

const isChecked =
currentUser.homepage !== undefined && isSameHomePage(currentUser.homepage, currentPage);
const isDefault = isChecked && isSameHomePage(currentPage, DEFAULT_HOMEPAGE);

const setCurrentUserHomepage = async (homepage: HomePage) => {
if (isLoggedIn(currentUser)) {
await setHomePage(homepage);

this.props.updateCurrentUserHomepage(homepage);
updateCurrentUserHomepage(homepage);
}
}

handleClick = () => {
this.setCurrentUserHomepage(this.props.currentPage);
};

handleReset = () => {
this.setCurrentUserHomepage(DEFAULT_HOMEPAGE);
};

render() {
const { className, currentPage, currentUser } = this.props;
const tooltip = isChecked
? intl.formatMessage({ id: isDefault ? 'homepage.current.is_default' : 'homepage.current' })
: intl.formatMessage({ id: 'homepage.check' });

if (!isLoggedIn(currentUser)) {
return null;
}
const handleClick = () => setCurrentUserHomepage?.(isChecked ? DEFAULT_HOMEPAGE : currentPage);

const { homepage } = currentUser;
const isChecked = homepage !== undefined && isSameHomePage(homepage, currentPage);
const isDefault = isChecked && isSameHomePage(currentPage, DEFAULT_HOMEPAGE);
const tooltip = isChecked
? translate(isDefault ? 'homepage.current.is_default' : 'homepage.current')
: translate('homepage.check');
const Icon = isChecked ? HomeFillIcon : HomeIcon;

return (
<Tooltip overlay={tooltip}>
return (
<Tooltip overlay={tooltip}>
{type === 'icon' ? (
<DiscreetInteractiveIcon
aria-label={tooltip}
className={className}
disabled={isDefault}
Icon={isChecked ? HomeFillIcon : HomeIcon}
onClick={isChecked ? this.handleReset : this.handleClick}
Icon={Icon}
onClick={handleClick}
/>
</Tooltip>
);
}
) : (
<ButtonSecondary
aria-label={tooltip}
icon={<Icon />}
className={className}
disabled={isDefault}
onClick={handleClick}
>
{intl.formatMessage({ id: 'overview.set_as_homepage' })}
</ButtonSecondary>
)}
</Tooltip>
);
}

export default withCurrentUserContext(HomePageSelect);

+ 2
- 1
server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx View File

@@ -23,6 +23,7 @@ import * as React from 'react';
import { setHomePage } from '../../../api/users';
import { mockLoggedInUser } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { FCProps } from '../../../types/misc';
import { DEFAULT_HOMEPAGE, HomePageSelect } from '../HomePageSelect';

jest.mock('../../../api/users', () => ({
@@ -56,7 +57,7 @@ it('renders correctly if user is on the homepage', async () => {
expect(button).toHaveFocus();
});

function renderHomePageSelect(props: Partial<HomePageSelect['props']> = {}) {
function renderHomePageSelect(props: Partial<FCProps<typeof HomePageSelect>> = {}) {
return renderComponent(
<HomePageSelect
currentPage={{ type: 'MY_PROJECTS' }}

+ 4
- 0
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx View File

@@ -28,6 +28,7 @@ import {
Title,
} from 'design-system';
import * as React from 'react';
import { AnalysisStatus } from '../../apps/overview/components/AnalysisStatus';
import { isMainBranch } from '../../helpers/branch-like';
import { translate } from '../../helpers/l10n';
import { getBaseUrl } from '../../helpers/system';
@@ -129,12 +130,15 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender

return (
<div className="sw-body-sm">
<AnalysisStatus component={component} className="sw-mb-4 sw-w-max" />

{selectedTutorial === undefined && (
<div className="sw-flex sw-flex-col">
<Title className="sw-mb-6 sw-heading-lg">
{translate('onboarding.tutorial.page.title')}
</Title>
<LightPrimary>{translate('onboarding.tutorial.page.description')}</LightPrimary>

<SubTitle className="sw-mt-12 sw-mb-4 sw-heading-md">
{translate('onboarding.tutorial.choose_method')}
</SubTitle>

+ 14
- 8
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx View File

@@ -103,7 +103,11 @@ export function renderAppWithAdminContext(
export function renderComponent(
component: React.ReactElement,
pathname = '/',
{ appState = mockAppState(), featureList = [] }: RenderContext = {},
{
appState = mockAppState(),
featureList = [],
currentUser = mockCurrentUser(),
}: RenderContext = {},
) {
function Wrapper({ children }: { children: React.ReactElement }) {
const queryClient = new QueryClient();
@@ -113,13 +117,15 @@ export function renderComponent(
<QueryClientProvider client={queryClient}>
<HelmetProvider>
<AvailableFeaturesContext.Provider value={featureList}>
<AppStateContextProvider appState={appState}>
<MemoryRouter initialEntries={[pathname]}>
<Routes>
<Route path="*" element={children} />
</Routes>
</MemoryRouter>
</AppStateContextProvider>
<CurrentUserContextProvider currentUser={currentUser}>
<AppStateContextProvider appState={appState}>
<MemoryRouter initialEntries={[pathname]}>
<Routes>
<Route path="*" element={children} />
</Routes>
</MemoryRouter>
</AppStateContextProvider>
</CurrentUserContextProvider>
</AvailableFeaturesContext.Provider>
</HelmetProvider>
</QueryClientProvider>

+ 2
- 0
server/sonar-web/src/main/js/types/component.ts View File

@@ -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 { Task } from './tasks';
import { Component, LightComponent } from './types';

export enum Visibility {
@@ -96,6 +97,7 @@ export function isView(

export interface ComponentContextShape {
component?: Component;
currentTask?: Task;
isInProgress?: boolean;
isPending?: boolean;
onComponentChange: (changes: Partial<Component>) => void;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -3895,6 +3895,7 @@ overview.quality_profiles=Quality Profiles used
overview.new_code_period_x=New Code: {0}
overview.max_new_code_period_from_x=Max New Code from: {0}
overview.started_x=Started {0}
overview.set_as_homepage=Set as homepage
overview.new_code=New Code
overview.overall_code=Overall Code
overview.last_analysis_x=Last analysis {date}

Loading…
Cancel
Save