From: stanislavh Date: Mon, 22 Jan 2024 16:52:24 +0000 (+0100) Subject: SONAR-21467 Implement new branch overview header X-Git-Tag: 10.4.0.87286~120 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=3efe35084f42d3a7815fdb298c12a407ac965585;p=sonarqube.git SONAR-21467 Implement new branch overview header --- 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( void; -} - -function isSameBranch(task: Task, branchLike?: BranchLike) { - if (branchLike) { - if (isMainBranch(branchLike)) { - return (!task.pullRequest && !task.branch) || branchLike.name === task.branch; - } - if (isPullRequest(branchLike)) { - return branchLike.key === task.pullRequest; - } - if (isBranch(branchLike)) { - return branchLike.name === task.branch; - } - } - return !task.branch && !task.pullRequest; -} - -export function AnalysisErrorMessage(props: Props) { - const { component, currentTask } = props; - const { data: { branchLike } = {} } = useBranchesQuery(component); - const currentTaskOnSameBranch = isSameBranch(currentTask, branchLike); - - const location = useLocation(); - - const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key); - const canSeeBackgroundTasks = component.configuration?.showBackgroundTasks; - const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname; - - const branch = - currentTask.branch ?? - `${currentTask.pullRequest ?? ''}${ - currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : '' - }`; - - let messageKey; - if (currentTaskOnSameBranch === false && branch) { - messageKey = 'component_navigation.status.failed_branch'; - } else { - messageKey = 'component_navigation.status.failed'; - } - - let type; - if (hasMessage('background_task.type', currentTask.type)) { - messageKey += '_X'; - type = translate('background_task.type', currentTask.type); - } - - let url; - let stacktrace; - if (canSeeBackgroundTasks) { - messageKey += '.admin'; - - if (isOnBackgroundTaskPage) { - messageKey += '.help'; - stacktrace = translate('background_tasks.show_stacktrace'); - } else { - messageKey += '.link'; - url = ( - - {translate('background_tasks.page')} - - ); - } - } - - return ( - - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx deleted file mode 100644 index 6005838bde0..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx +++ /dev/null @@ -1,67 +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 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'; - -interface Props { - component: Component; - currentTask: Task; - onClose: () => void; -} - -export function AnalysisErrorModal(props: Props) { - const { component, currentTask } = props; - - const header = translate('error'); - - const licenseError = - currentTask.errorType && - hasMessage('license.component_navigation.button', currentTask.errorType); - - return ( - -
-

{header}

-
- -
- {licenseError ? ( - - ) : ( - - )} -
- -
- {translate('close')} -
-
- ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx deleted file mode 100644 index 38a518142cf..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx +++ /dev/null @@ -1,64 +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 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 { useLicenseIsValid } from './useLicenseIsValid'; - -interface Props { - currentTask: Task; -} - -export function AnalysisLicenseError(props: Props) { - const { currentTask } = props; - const appState = React.useContext(AppStateContext); - const [licenseIsValid, loading] = useLicenseIsValid(); - - if (loading || !currentTask.errorType) { - return null; - } - - if (licenseIsValid && currentTask.errorType !== 'LICENSING_LOC') { - return ( - <> - {translateWithParameters( - 'component_navigation.status.last_blocked_due_to_bad_license_X', - translate('qualifier', currentTask.componentQualifier ?? ComponentQualifier.Project), - )} - - ); - } - - return ( - <> - {currentTask.errorMessage} - {appState.canAdmin ? ( - - {translate('license.component_navigation.button', currentTask.errorType)}. - - ) : ( - translate('please_contact_administrator') - )} - - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx deleted file mode 100644 index b6408f02012..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx +++ /dev/null @@ -1,98 +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 { 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 { AnalysisErrorModal } from './AnalysisErrorModal'; -import AnalysisWarningsModal from './AnalysisWarningsModal'; - -export interface HeaderMetaProps { - currentTask?: Task; - component: Component; - isInProgress?: boolean; - isPending?: boolean; -} - -export function AnalysisStatus(props: HeaderMetaProps) { - const { component, currentTask, isInProgress, isPending } = props; - const { data: warnings, isLoading } = useBranchWarningQuery(component); - - const [modalIsVisible, setDisplayModal] = React.useState(false); - const openModal = React.useCallback(() => { - setDisplayModal(true); - }, [setDisplayModal]); - const closeModal = React.useCallback(() => { - setDisplayModal(false); - }, [setDisplayModal]); - - if (isInProgress || isPending) { - return ( -
- - - {isInProgress - ? translate('project_navigation.analysis_status.in_progress') - : translate('project_navigation.analysis_status.pending')} - -
- ); - } - - if (currentTask?.status === TaskStatuses.Failed) { - return ( - <> - - {translate('project_navigation.analysis_status.failed')} - - {translate('project_navigation.analysis_status.details_link')} - - - {modalIsVisible && ( - - )} - - ); - } - - if (!isLoading && warnings && warnings.length > 0) { - return ( - <> - - {translate('project_navigation.analysis_status.warnings')} - - {translate('project_navigation.analysis_status.details_link')} - - - {modalIsVisible && ( - - )} - - ); - } - - return null; -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx deleted file mode 100644 index 45914a27ae3..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx +++ /dev/null @@ -1,94 +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 { 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'; - -interface Props { - component: Component; - currentUser: CurrentUser; - onClose: () => void; - warnings: TaskWarning[]; -} - -export function AnalysisWarningsModal(props: Props) { - const { component, currentUser, warnings } = props; - - const { mutate, isLoading, variables } = useDismissBranchWarningMutation(); - - const handleDismissMessage = (messageKey: string) => { - mutate({ component, key: messageKey }); - }; - - const body = ( - <> - {warnings.map(({ dismissable, key, message }) => ( - -
- - - ')), - }} - /> - - -
-
- {dismissable && currentUser.isLoggedIn && ( -
- { - handleDismissMessage(key); - }} - > - {translate('dismiss_permanently')} - - - -
- )} -
-
- ))} - - ); - - return ( - - ); -} - -export default withCurrentUserContext(AnalysisWarningsModal); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 206b350658c..cab37894a90 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -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) { - 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__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx deleted file mode 100644 index b30d3bc0ca4..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx +++ /dev/null @@ -1,93 +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 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(); - -beforeEach(() => { - handler.reset(); -}); - -it('should work when error is on a different branch', () => { - renderAnalysisErrorMessage({ - currentTask: mockTask({ branch: 'branch-1.2' }), - }); - - expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument(); - expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument(); -}); - -it('should work for errors on Pull Requests', async () => { - renderAnalysisErrorMessage( - { - currentTask: mockTask({ pullRequest: '01', pullRequestTitle: 'Fix stuff' }), - }, - undefined, - 'pullRequest=01&id=my-project', - ); - - expect(await screen.findByText(/component_navigation.status.failed_X/)).toBeInTheDocument(); - expect(screen.getByText(/01 - Fix stuff/)).toBeInTheDocument(); -}); - -it('should provide a link to admins', () => { - renderAnalysisErrorMessage({ - component: mockComponent({ configuration: { showBackgroundTasks: true } }), - }); - - expect(screen.getByText(/component_navigation.status.failed_X.admin.link/)).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'background_tasks.page' })).toBeInTheDocument(); -}); - -it('should explain to admins how to get the staktrace', () => { - renderAnalysisErrorMessage( - { - component: mockComponent({ configuration: { showBackgroundTasks: true } }), - }, - 'project/background_tasks', - ); - - expect(screen.getByText(/component_navigation.status.failed_X.admin.help/)).toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'background_tasks.page' })).not.toBeInTheDocument(); -}); - -function renderAnalysisErrorMessage( - overrides: Partial[0]> = {}, - location = '/', - params?: string, -) { - return renderApp( - location, - , - { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] }, - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx deleted file mode 100644 index 43ffde14cbb..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx +++ /dev/null @@ -1,78 +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 { 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', () => ({ - isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }), -})); - -it('should handle a valid license', async () => { - renderAnalysisLicenseError({ - currentTask: mockTask({ errorType: 'ANY_TYPE' }), - }); - - expect( - await screen.findByText( - 'component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK', - ), - ).toBeInTheDocument(); -}); - -it('should send user to contact the admin', async () => { - const errorMessage = 'error message'; - renderAnalysisLicenseError({ - currentTask: mockTask({ errorMessage, errorType: 'LICENSING_LOC' }), - }); - - expect(await screen.findByText('please_contact_administrator')).toBeInTheDocument(); - expect(screen.getByText(errorMessage)).toBeInTheDocument(); -}); - -it('should send provide a link to the admin', async () => { - jest.mocked(isValidLicense).mockResolvedValueOnce({ isValidLicense: false }); - - const errorMessage = 'error message'; - renderAnalysisLicenseError( - { - currentTask: mockTask({ errorMessage, errorType: 'error-type' }), - }, - true, - ); - - expect( - await screen.findByText('license.component_navigation.button.error-type.'), - ).toBeInTheDocument(); - expect(screen.getByText(errorMessage)).toBeInTheDocument(); -}); - -function renderAnalysisLicenseError( - overrides: Partial[0]> = {}, - canAdmin = false, -) { - return renderApp('/', , { - appState: mockAppState({ canAdmin }), - }); -} 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/app/components/nav/component/useLicenseIsValid.ts b/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts deleted file mode 100644 index 979e0095a9b..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts +++ /dev/null @@ -1,41 +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 React, { useEffect } from 'react'; -import { isValidLicense } from '../../../../api/editions'; - -export function useLicenseIsValid(): [boolean, boolean] { - const [licenseIsValid, setLicenseIsValid] = React.useState(false); - const [loading, setLoading] = React.useState(true); - - useEffect(() => { - setLoading(true); - - isValidLicense() - .then(({ isValidLicense }) => { - setLicenseIsValid(isValidLicense); - setLoading(false); - }) - .catch(() => { - setLoading(false); - }); - }, []); - - return [licenseIsValid, loading]; -} 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/apps/overview/components/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx new file mode 100644 index 00000000000..8a42b1d2c97 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx @@ -0,0 +1,108 @@ +/* + * 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 { 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'; + +interface Props { + component: Component; + currentTask: Task; + onLeave: () => void; +} + +function isSameBranch(task: Task, branchLike?: BranchLike) { + if (branchLike) { + if (isMainBranch(branchLike)) { + return (!task.pullRequest && !task.branch) || branchLike.name === task.branch; + } + if (isPullRequest(branchLike)) { + return branchLike.key === task.pullRequest; + } + if (isBranch(branchLike)) { + return branchLike.name === task.branch; + } + } + return !task.branch && !task.pullRequest; +} + +export function AnalysisErrorMessage(props: Props) { + const { component, currentTask } = props; + const { data: { branchLike } = {} } = useBranchesQuery(component); + const currentTaskOnSameBranch = isSameBranch(currentTask, branchLike); + + const location = useLocation(); + + const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key); + const canSeeBackgroundTasks = component.configuration?.showBackgroundTasks; + const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname; + + const branch = + currentTask.branch ?? + `${currentTask.pullRequest ?? ''}${ + currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : '' + }`; + + let messageKey; + if (currentTaskOnSameBranch === false && branch) { + messageKey = 'component_navigation.status.failed_branch'; + } else { + messageKey = 'component_navigation.status.failed'; + } + + let type; + if (hasMessage('background_task.type', currentTask.type)) { + messageKey += '_X'; + type = translate('background_task.type', currentTask.type); + } + + let url; + let stacktrace; + if (canSeeBackgroundTasks) { + messageKey += '.admin'; + + if (isOnBackgroundTaskPage) { + messageKey += '.help'; + stacktrace = translate('background_tasks.show_stacktrace'); + } else { + messageKey += '.link'; + url = ( + + {translate('background_tasks.page')} + + ); + } + } + + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx new file mode 100644 index 00000000000..3158d29fc17 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx @@ -0,0 +1,67 @@ +/* + * 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 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'; + +interface Props { + component: Component; + currentTask: Task; + onClose: () => void; +} + +export function AnalysisErrorModal(props: Props) { + const { component, currentTask } = props; + + const header = translate('error'); + + const licenseError = + currentTask.errorType && + hasMessage('license.component_navigation.button', currentTask.errorType); + + return ( + +
+

{header}

+
+ +
+ {licenseError ? ( + + ) : ( + + )} +
+ +
+ {translate('close')} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx new file mode 100644 index 00000000000..a8f370a1f0b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx @@ -0,0 +1,64 @@ +/* + * 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 { 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 { + currentTask: Task; +} + +export function AnalysisLicenseError(props: Props) { + const { currentTask } = props; + const appState = React.useContext(AppStateContext); + const [licenseIsValid, loading] = useLicenseIsValid(); + + if (loading || !currentTask.errorType) { + return null; + } + + if (licenseIsValid && currentTask.errorType !== 'LICENSING_LOC') { + return ( + <> + {translateWithParameters( + 'component_navigation.status.last_blocked_due_to_bad_license_X', + translate('qualifier', currentTask.componentQualifier ?? ComponentQualifier.Project), + )} + + ); + } + + return ( + <> + {currentTask.errorMessage} + {appState.canAdmin ? ( + + {translate('license.component_navigation.button', currentTask.errorType)}. + + ) : ( + translate('please_contact_administrator') + )} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx new file mode 100644 index 00000000000..df14b944f1c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx @@ -0,0 +1,99 @@ +/* + * 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 classNames from 'classnames'; +import { FlagMessage, Link, Spinner } from 'design-system'; +import * as React from 'react'; +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 { + component: Component; + className?: string; +} + +export function AnalysisStatus(props: HeaderMetaProps) { + const { className, component } = props; + const { currentTask, isPending, isInProgress } = useComponent(); + const { data: warnings, isLoading } = useBranchWarningQuery(component); + + const [modalIsVisible, setDisplayModal] = React.useState(false); + const openModal = React.useCallback(() => { + setDisplayModal(true); + }, [setDisplayModal]); + const closeModal = React.useCallback(() => { + setDisplayModal(false); + }, [setDisplayModal]); + + if (isInProgress || isPending) { + return ( +
+ + + {isInProgress + ? translate('project_navigation.analysis_status.in_progress') + : translate('project_navigation.analysis_status.pending')} + +
+ ); + } + + if (currentTask?.status === TaskStatuses.Failed) { + return ( + <> + + {translate('project_navigation.analysis_status.failed')} + + {translate('project_navigation.analysis_status.details_link')} + + + {modalIsVisible && ( + + )} + + ); + } + + if (!isLoading && warnings && warnings.length > 0) { + return ( + <> + + {translate('project_navigation.analysis_status.warnings')} + + {translate('project_navigation.analysis_status.details_link')} + + + {modalIsVisible && ( + + )} + + ); + } + + return null; +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx new file mode 100644 index 00000000000..d17e2f71782 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx @@ -0,0 +1,94 @@ +/* + * 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 { DangerButtonSecondary, FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system'; +import * as React from 'react'; +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; + currentUser: CurrentUser; + onClose: () => void; + warnings: TaskWarning[]; +} + +export function AnalysisWarningsModal(props: Props) { + const { component, currentUser, warnings } = props; + + const { mutate, isLoading, variables } = useDismissBranchWarningMutation(); + + const handleDismissMessage = (messageKey: string) => { + mutate({ component, key: messageKey }); + }; + + const body = ( + <> + {warnings.map(({ dismissable, key, message }) => ( + +
+ + + ')), + }} + /> + + +
+
+ {dismissable && currentUser.isLoggedIn && ( +
+ { + handleDismissMessage(key); + }} + > + {translate('dismiss_permanently')} + + + +
+ )} +
+
+ ))} + + ); + + return ( + + ); +} + +export default withCurrentUserContext(AnalysisWarningsModal); 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/apps/overview/components/MetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx deleted file mode 100644 index bfe219ae6f0..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx +++ /dev/null @@ -1,79 +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 React from 'react'; -import { useIntl } from 'react-intl'; -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 { MetricKey, MetricType } from '../../../types/metrics'; -import { MeasureEnhanced } from '../../../types/types'; - -interface Props { - branchLike: BranchLike; - measures: MeasureEnhanced[]; -} - -export default function MetaTopBar({ branchLike, 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} -
- ); - const rightSection = ( -
- {branchLike.analysisDate - ? intl.formatMessage( - { - id: 'overview.last_analysis_x', - }, - { - date: ( - - - - ), - }, - ) - : null} -
- ); - - return ( -
- {leftSection} - {rightSection} -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx new file mode 100644 index 00000000000..0958050e43f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx @@ -0,0 +1,93 @@ +/* + * 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 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(); + +beforeEach(() => { + handler.reset(); +}); + +it('should work when error is on a different branch', () => { + renderAnalysisErrorMessage({ + currentTask: mockTask({ branch: 'branch-1.2' }), + }); + + expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument(); + expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument(); +}); + +it('should work for errors on Pull Requests', async () => { + renderAnalysisErrorMessage( + { + currentTask: mockTask({ pullRequest: '01', pullRequestTitle: 'Fix stuff' }), + }, + undefined, + 'pullRequest=01&id=my-project', + ); + + expect(await screen.findByText(/component_navigation.status.failed_X/)).toBeInTheDocument(); + expect(screen.getByText(/01 - Fix stuff/)).toBeInTheDocument(); +}); + +it('should provide a link to admins', () => { + renderAnalysisErrorMessage({ + component: mockComponent({ configuration: { showBackgroundTasks: true } }), + }); + + expect(screen.getByText(/component_navigation.status.failed_X.admin.link/)).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'background_tasks.page' })).toBeInTheDocument(); +}); + +it('should explain to admins how to get the staktrace', () => { + renderAnalysisErrorMessage( + { + component: mockComponent({ configuration: { showBackgroundTasks: true } }), + }, + 'project/background_tasks', + ); + + expect(screen.getByText(/component_navigation.status.failed_X.admin.help/)).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'background_tasks.page' })).not.toBeInTheDocument(); +}); + +function renderAnalysisErrorMessage( + overrides: Partial[0]> = {}, + location = '/', + params?: string, +) { + return renderApp( + location, + , + { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] }, + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx new file mode 100644 index 00000000000..c5a8b5f44c2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx @@ -0,0 +1,78 @@ +/* + * 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 { 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', () => ({ + isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }), +})); + +it('should handle a valid license', async () => { + renderAnalysisLicenseError({ + currentTask: mockTask({ errorType: 'ANY_TYPE' }), + }); + + expect( + await screen.findByText( + 'component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK', + ), + ).toBeInTheDocument(); +}); + +it('should send user to contact the admin', async () => { + const errorMessage = 'error message'; + renderAnalysisLicenseError({ + currentTask: mockTask({ errorMessage, errorType: 'LICENSING_LOC' }), + }); + + expect(await screen.findByText('please_contact_administrator')).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); +}); + +it('should send provide a link to the admin', async () => { + jest.mocked(isValidLicense).mockResolvedValueOnce({ isValidLicense: false }); + + const errorMessage = 'error message'; + renderAnalysisLicenseError( + { + currentTask: mockTask({ errorMessage, errorType: 'error-type' }), + }, + true, + ); + + expect( + await screen.findByText('license.component_navigation.button.error-type.'), + ).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); +}); + +function renderAnalysisLicenseError( + overrides: Partial[0]> = {}, + canAdmin = false, +) { + return renderApp('/', , { + appState: mockAppState({ canAdmin }), + }); +} 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/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx new file mode 100644 index 00000000000..4f3e735b19e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx @@ -0,0 +1,78 @@ +/* + * 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 { 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', () => ({ + dismissAnalysisWarning: jest.fn().mockResolvedValue(null), + getTask: jest.fn().mockResolvedValue({ + warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n third line'], + }), +})); + +beforeEach(jest.clearAllMocks); + +describe('should render correctly', () => { + it('should not show dismiss buttons for non-dismissable warnings', () => { + renderAnalysisWarningsModal(); + + expect(screen.getByText('warning 1')).toBeInTheDocument(); + expect(screen.getByText('warning 2')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument(); + }); + + it('should show a dismiss button for dismissable warnings', () => { + renderAnalysisWarningsModal({ warnings: [mockTaskWarning({ dismissable: true })] }); + + expect(screen.getByRole('button', { name: 'dismiss_permanently' })).toBeInTheDocument(); + }); + + it('should not show dismiss buttons if not logged in', () => { + renderAnalysisWarningsModal({ + currentUser: mockCurrentUser({ isLoggedIn: false }), + warnings: [mockTaskWarning({ dismissable: true })], + }); + + expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument(); + }); +}); + +function renderAnalysisWarningsModal( + props: Partial> = {}, +) { + return renderComponent( + , + ); +} 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/apps/overview/components/useLicenseIsValid.ts b/server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts new file mode 100644 index 00000000000..8693d6b5fbc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts @@ -0,0 +1,41 @@ +/* + * 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 React, { useEffect } from 'react'; +import { isValidLicense } from '../../../api/editions'; + +export function useLicenseIsValid(): [boolean, boolean] { + const [licenseIsValid, setLicenseIsValid] = React.useState(false); + const [loading, setLoading] = React.useState(true); + + useEffect(() => { + setLoading(true); + + isValidLicense() + .then(({ isValidLicense }) => { + setLicenseIsValid(isValidLicense); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + }, []); + + return [licenseIsValid, loading]; +} diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx new file mode 100644 index 00000000000..72c4a6a35cc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx @@ -0,0 +1,80 @@ +/* + * 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 CurrentBranchLikeMergeInformation from '../../../app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation'; +import DateFromNow from '../../../components/intl/DateFromNow'; +import { getLeakValue } from '../../../components/measure/utils'; +import { findMeasure, formatMeasure } from '../../../helpers/measures'; +import { PullRequest } from '../../../types/branch-like'; +import { MetricKey, MetricType } from '../../../types/metrics'; +import { MeasureEnhanced } from '../../../types/types'; + +interface Props { + pullRequest: PullRequest; + measures: MeasureEnhanced[]; +} + +export default function PullRequestMetaTopBar({ pullRequest, measures }: Readonly) { + const intl = useIntl(); + + const leftSection = ( +
+ + {formatMeasure( + getLeakValue(findMeasure(measures, MetricKey.new_lines)), + MetricType.ShortInteger, + ) || '0'} + + {intl.formatMessage({ id: 'metric.new_lines.name' })} +
+ ); + const rightSection = ( +
+ + + {pullRequest.analysisDate && ( + <> + + {intl.formatMessage( + { + id: 'overview.last_analysis_x', + }, + { + date: ( + + + + ), + }, + )} + + )} +
+ ); + + return ( +
+ {leftSection} + {rightSection} +
+ ); +} 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/common/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx deleted file mode 100644 index 33f0295c772..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx +++ /dev/null @@ -1,78 +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 { 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'; - -jest.mock('../../../api/ce', () => ({ - dismissAnalysisWarning: jest.fn().mockResolvedValue(null), - getTask: jest.fn().mockResolvedValue({ - warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n third line'], - }), -})); - -beforeEach(jest.clearAllMocks); - -describe('should render correctly', () => { - it('should not show dismiss buttons for non-dismissable warnings', () => { - renderAnalysisWarningsModal(); - - expect(screen.getByText('warning 1')).toBeInTheDocument(); - expect(screen.getByText('warning 2')).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument(); - }); - - it('should show a dismiss button for dismissable warnings', () => { - renderAnalysisWarningsModal({ warnings: [mockTaskWarning({ dismissable: true })] }); - - expect(screen.getByRole('button', { name: 'dismiss_permanently' })).toBeInTheDocument(); - }); - - it('should not show dismiss buttons if not logged in', () => { - renderAnalysisWarningsModal({ - currentUser: mockCurrentUser({ isLoggedIn: false }), - warnings: [mockTaskWarning({ dismissable: true })], - }); - - expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument(); - }); -}); - -function renderAnalysisWarningsModal( - props: Partial> = {}, -) { - return renderComponent( - , - ); -} 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}