From 3efe35084f42d3a7815fdb298c12a407ac965585 Mon Sep 17 00:00:00 2001 From: stanislavh Date: Mon, 22 Jan 2024 17:52:24 +0100 Subject: [PATCH] SONAR-21467 Implement new branch overview header --- .../js/api/mocks/ComputeEngineServiceMock.ts | 28 +++- .../js/app/components/ComponentContainer.tsx | 4 +- .../componentContext/withComponentContext.tsx | 4 + .../components/nav/component/ComponentNav.tsx | 20 +-- .../components/nav/component/HeaderMeta.tsx | 73 --------- .../component/__tests__/ComponentNav-test.tsx | 23 --- .../component/__tests__/HeaderMeta-test.tsx | 119 --------------- .../CurrentBranchLikeMergeInformation.tsx | 25 ++-- .../overview/branches/BranchMetaTopBar.tsx | 77 ++++++++++ .../branches/BranchOverviewRenderer.tsx | 116 ++++++++------- .../js/apps/overview/branches/TabsPanel.tsx | 7 - .../branches/__tests__/BranchOverview-it.tsx | 7 +- .../components}/AnalysisErrorMessage.tsx | 14 +- .../components}/AnalysisErrorModal.tsx | 10 +- .../components}/AnalysisLicenseError.tsx | 10 +- .../overview/components}/AnalysisStatus.tsx | 23 +-- .../components}/AnalysisWarningsModal.tsx | 14 +- .../overview/components/MeasurementLabel.tsx | 138 ------------------ .../__tests__/AnalysisErrorMessage-test.tsx | 10 +- .../__tests__/AnalysisLicenseError-test.tsx | 10 +- .../__tests__/AnalysisStatus-it.tsx | 107 ++++++++++++++ .../__tests__/AnalysisWarningsModal-test.tsx | 14 +- .../__tests__/MeasurementLabel-test.tsx | 113 -------------- .../overview/components}/useLicenseIsValid.ts | 2 +- .../PullRequestMetaTopBar.tsx} | 45 +++--- .../pullRequests/PullRequestOverview.tsx | 7 +- .../js/components/controls/HomePageSelect.tsx | 73 ++++----- .../__tests__/HomePageSelect-test.tsx | 3 +- .../tutorials/TutorialSelectionRenderer.tsx | 4 + .../main/js/helpers/testReactTestingUtils.tsx | 22 ++- .../sonar-web/src/main/js/types/component.ts | 2 + .../resources/org/sonar/l10n/core.properties | 1 + 32 files changed, 443 insertions(+), 682 deletions(-) delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/AnalysisErrorMessage.tsx (89%) rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/AnalysisErrorModal.tsx (86%) rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/AnalysisLicenseError.tsx (85%) rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/AnalysisStatus.tsx (79%) rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/AnalysisWarningsModal.tsx (86%) delete mode 100644 server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/__tests__/AnalysisErrorMessage-test.tsx (89%) rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/__tests__/AnalysisLicenseError-test.tsx (88%) create mode 100644 server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx rename server/sonar-web/src/main/js/{components/common => apps/overview/components}/__tests__/AnalysisWarningsModal-test.tsx (84%) delete mode 100644 server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx rename server/sonar-web/src/main/js/{app/components/nav/component => apps/overview/components}/useLicenseIsValid.ts (95%) rename server/sonar-web/src/main/js/apps/overview/{components/MetaTopBar.tsx => pullRequests/PullRequestMetaTopBar.tsx} (65%) diff --git a/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts index e5746f7ffae..71abfecd79d 100644 --- a/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts @@ -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 }; } diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index 829359c6353..b8edf5e2c4d 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -258,12 +258,13 @@ function ComponentContainer({ hasFeature }: Readonly 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 createPortal( ) { - 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) {
-
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx deleted file mode 100644 index b6ed7b4ab3e..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx +++ /dev/null @@ -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 ( -
- - {branchLike && } - {component.version !== undefined && isABranch && ( - - )} - {isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && ( - - )} -
- ); -} - -export default withCurrentUserContext(HeaderMeta); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index 9ed116e208e..c711da7c938 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -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(), diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx deleted file mode 100644 index 5685ac03b60..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx +++ /dev/null @@ -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 = {}, - currentUser: CurrentUser = mockLoggedInUser(), - params?: string, -) { - return renderApp('/', , { - currentUser, - navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project', - featureList: [Feature.BranchSupport], - }); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx index bdb7bd1e267..d8d7c24da37 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx @@ -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) { return ( {currentBranchLike.target}, - branch: {currentBranchLike.branch}, + target: {pullRequest.target}, + branch: {pullRequest.branch}, }} /> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx new file mode 100644 index 00000000000..d40efb4446b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx @@ -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) { + const intl = useIntl(); + + const currentPage = getCurrentPage(component, branch) as HomePage; + const locMeasure = findMeasure(measures, MetricKey.lines); + + const leftSection = ( +

{branch.name}

+ ); + const rightSection = ( +
+ {locMeasure && ( + <> +
+ {formatMeasure(locMeasure.value, MetricType.ShortInteger)} + {intl.formatMessage({ id: 'metric.ncloc.name' })} +
+ + + )} + {component.version && ( + <> +
+ {intl.formatMessage({ id: 'version_x' }, { '0': {component.version} })} +
+ + + )} + + +
+ ); + + return ( +
+ {leftSection} + {rightSection} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx index 4bccbf849fc..0dca8d3241e 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx @@ -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 ? ( ) : ( -
-
- -
- -
-
- + {branch && ( + <> + + + + )} + +
+
+ - {!hasNewCodeMeasures && isNewCodeTab ? ( - - ) : ( - <> - + qualityGate={qualityGate} + /> +
- +
+ + {!hasNewCodeMeasures && isNewCodeTab ? ( + - - )} - + ) : ( + <> + - + + + )} + + + +
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx index 258dffa8b6e..9756c817ae9 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx @@ -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) { const { analyses, appLeak, - branch, component, loading, period, @@ -128,9 +124,6 @@ export function TabsPanel(props: React.PropsWithChildren) { return (
-
- -
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx index b074b53a63b..b06c4671c1e 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx @@ -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 = {}) { breadcrumbs: [mockComponent({ key: 'foo' })], key: 'foo', name: 'Foo', + version: 'version-1.0', })} {...props} /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx similarity index 89% rename from server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx rename to server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx index d9536cd615a..8a42b1d2c97 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx @@ -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; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx similarity index 86% rename from server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx rename to server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx index 6005838bde0..3158d29fc17 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx similarity index 85% rename from server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx rename to server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx index 38a518142cf..a8f370a1f0b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx @@ -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 { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx similarity index 79% rename from server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx rename to server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx index b6408f02012..df14b944f1c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx @@ -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 ( -
+
{isInProgress @@ -61,7 +62,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { if (currentTask?.status === TaskStatuses.Failed) { return ( <> - + {translate('project_navigation.analysis_status.failed')} {translate('project_navigation.analysis_status.details_link')} @@ -81,7 +82,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { if (!isLoading && warnings && warnings.length > 0) { return ( <> - + {translate('project_navigation.analysis_status.warnings')} {translate('project_navigation.analysis_status.details_link')} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx similarity index 86% rename from server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx rename to server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx index 45914a27ae3..d17e2f71782 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx @@ -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; diff --git a/server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx b/server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx deleted file mode 100644 index 8b68d386c02..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx +++ /dev/null @@ -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 { - 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 ( - - {formatMeasure(value, 'SHORT_INT')} - - ), - }} - /> - ); - }; - - 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 ( -
- - {this.getLabelText()} -
- ); - } - - const icon = React.createElement(iconClass, { size: 'big', value: Number(value) }); - const formattedValue = formatMeasure(value, 'PERCENT', { - decimals: 2, - omitExtraDecimalZeros: true, - }); - const link = ( - - {formattedValue} - - ); - const label = this.getLabelText(); - - return centered ? ( -
-
- {icon} - {link} -
-
{label}
-
- ) : ( -
- {icon} -
- {link} - {label} -
-
- ); - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx similarity index 89% rename from server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx rename to server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx index b30d3bc0ca4..0958050e43f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx similarity index 88% rename from server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx rename to server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx index 43ffde14cbb..c5a8b5f44c2 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx @@ -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 }), })); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx new file mode 100644 index 00000000000..921fc5c9cad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx @@ -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[0]> = {}) { + return renderComponent( + , + '/?id=my-project', + ); +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx similarity index 84% rename from server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx rename to server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx index 33f0295c772..4f3e735b19e 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx @@ -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'], diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx deleted file mode 100644 index 5453149acaf..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx +++ /dev/null @@ -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 = {}) { - return renderComponent( - , - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts b/server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts similarity index 95% rename from server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts rename to server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts index 979e0095a9b..8693d6b5fbc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts +++ b/server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts @@ -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); diff --git a/server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx similarity index 65% rename from server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx rename to server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx index bfe219ae6f0..72c4a6a35cc 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx @@ -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) { +export default function PullRequestMetaTopBar({ pullRequest, measures }: Readonly) { const intl = useIntl(); - const isPR = isPullRequest(branchLike); const leftSection = (
- {isPR ? ( - <> - - {formatMeasure( - getLeakValue(findMeasure(measures, MetricKey.new_lines)), - MetricType.ShortInteger, - ) ?? '0'} - - {intl.formatMessage({ id: 'metric.new_lines.name' })} - - ) : null} + + {formatMeasure( + getLeakValue(findMeasure(measures, MetricKey.new_lines)), + MetricType.ShortInteger, + ) || '0'} + + {intl.formatMessage({ id: 'metric.new_lines.name' })}
); const rightSection = ( -
- {branchLike.analysisDate - ? intl.formatMessage( +
+ + + {pullRequest.analysisDate && ( + <> + + {intl.formatMessage( { id: 'overview.last_analysis_x', }, { date: ( - + ), }, - ) - : null} + )} + + )}
); diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx index 3de4a392362..5e7c411c033 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx @@ -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>) {
- + + + {ignoredConditions && } {status && ( diff --git a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx index 8803258c32d..e7d6e2ff1af 100644 --- a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx +++ b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx @@ -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 { className?: string; currentPage: HomePage; + type?: 'button' | 'icon'; } export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' }; -export class HomePageSelect extends React.PureComponent { - async setCurrentUserHomepage(homepage: HomePage) { - const { currentUser } = this.props; +export function HomePageSelect(props: Readonly) { + 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 ( - + return ( + + {type === 'icon' ? ( - - ); - } + ) : ( + } + className={className} + disabled={isDefault} + onClick={handleClick} + > + {intl.formatMessage({ id: 'overview.set_as_homepage' })} + + )} + + ); } export default withCurrentUserContext(HomePageSelect); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx index 87f65cdda54..e3ef11db92c 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx @@ -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 = {}) { +function renderHomePageSelect(props: Partial> = {}) { return renderComponent( + + {selectedTutorial === undefined && (
{translate('onboarding.tutorial.page.title')} {translate('onboarding.tutorial.page.description')} + {translate('onboarding.tutorial.choose_method')} diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index b177ff6b9a5..7b02bac6fb1 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -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( - - - - - - - + + + + + + + + + diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts index 4a9e7fa67e8..416034aba9b 100644 --- a/server/sonar-web/src/main/js/types/component.ts +++ b/server/sonar-web/src/main/js/types/component.ts @@ -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) => void; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 5aba25d721d..58ddb11c238 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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} -- 2.39.5