@@ -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 }; | |||
} | |||
@@ -258,12 +258,13 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps> | |||
const componentProviderProps = React.useMemo( | |||
() => ({ | |||
component, | |||
currentTask, | |||
isInProgress, | |||
isPending, | |||
onComponentChange: handleComponentChange, | |||
fetchComponent, | |||
}), | |||
[component, isInProgress, isPending, handleComponentChange, fetchComponent], | |||
[component, currentTask, isInProgress, isPending, handleComponentChange, fetchComponent], | |||
); | |||
// Show not found component when, after loading: | |||
@@ -289,7 +290,6 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps> | |||
createPortal( | |||
<ComponentNav | |||
component={component} | |||
currentTask={currentTask} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
projectBindingErrors={projectBindingErrors} |
@@ -39,3 +39,7 @@ export default function withComponentContext<P extends Partial<ComponentContextS | |||
} | |||
}; | |||
} | |||
export function useComponent() { | |||
return React.useContext(ComponentContext); | |||
} |
@@ -26,7 +26,6 @@ import { ProjectAlmBindingConfigurationErrors } from '../../../../types/alm-sett | |||
import { Branch } from '../../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Feature } from '../../../../types/features'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import RecentHistory from '../../RecentHistory'; | |||
import withAvailableFeatures, { | |||
@@ -34,28 +33,19 @@ import withAvailableFeatures, { | |||
} from '../../available-features/withAvailableFeatures'; | |||
import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif'; | |||
import Header from './Header'; | |||
import HeaderMeta from './HeaderMeta'; | |||
import Menu from './Menu'; | |||
export interface ComponentNavProps extends WithAvailableFeaturesProps { | |||
branchLike?: Branch; | |||
component: Component; | |||
currentTask?: Task; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
projectBindingErrors?: ProjectAlmBindingConfigurationErrors; | |||
} | |||
function ComponentNav(props: Readonly<ComponentNavProps>) { | |||
const { | |||
branchLike, | |||
component, | |||
currentTask, | |||
hasFeature, | |||
isInProgress, | |||
isPending, | |||
projectBindingErrors, | |||
} = props; | |||
const { branchLike, component, hasFeature, isInProgress, isPending, projectBindingErrors } = | |||
props; | |||
React.useEffect(() => { | |||
const { breadcrumbs, key, name } = component; | |||
@@ -76,12 +66,6 @@ function ComponentNav(props: Readonly<ComponentNavProps>) { | |||
<TopBar id="context-navigation" aria-label={translate('qualifier', component.qualifier)}> | |||
<div className="sw-min-h-10 sw-flex sw-justify-between"> | |||
<Header component={component} /> | |||
<HeaderMeta | |||
component={component} | |||
currentTask={currentTask} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
/> | |||
</div> | |||
<Menu component={component} isInProgress={isInProgress} isPending={isPending} /> | |||
</TopBar> |
@@ -1,73 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { TextMuted } from 'design-system'; | |||
import * as React from 'react'; | |||
import HomePageSelect from '../../../../components/controls/HomePageSelect'; | |||
import { isBranch, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { translateWithParameters } from '../../../../helpers/l10n'; | |||
import { useBranchesQuery } from '../../../../queries/branch'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { CurrentUser, isLoggedIn } from '../../../../types/users'; | |||
import withCurrentUserContext from '../../current-user/withCurrentUserContext'; | |||
import { AnalysisStatus } from './AnalysisStatus'; | |||
import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation'; | |||
import { getCurrentPage } from './utils'; | |||
export interface HeaderMetaProps { | |||
component: Component; | |||
currentUser: CurrentUser; | |||
currentTask?: Task; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
} | |||
export function HeaderMeta(props: HeaderMetaProps) { | |||
const { component, currentUser, currentTask, isInProgress, isPending } = props; | |||
const { data: { branchLike } = {} } = useBranchesQuery(component); | |||
const isABranch = isBranch(branchLike); | |||
const currentPage = getCurrentPage(component, branchLike); | |||
return ( | |||
<div className="sw-flex sw-items-center sw-flex-shrink sw-min-w-0"> | |||
<AnalysisStatus | |||
component={component} | |||
currentTask={currentTask} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
/> | |||
{branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />} | |||
{component.version !== undefined && isABranch && ( | |||
<TextMuted | |||
text={translateWithParameters('version_x', component.version)} | |||
className="sw-ml-4 sw-whitespace-nowrap" | |||
/> | |||
)} | |||
{isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && ( | |||
<HomePageSelect className="sw-ml-2" currentPage={currentPage} /> | |||
)} | |||
</div> | |||
); | |||
} | |||
export default withCurrentUserContext(HeaderMeta); |
@@ -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(), |
@@ -1,119 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { getAnalysisStatus } from '../../../../../api/ce'; | |||
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { mockTask } from '../../../../../helpers/mocks/tasks'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../../../types/features'; | |||
import { TaskStatuses } from '../../../../../types/tasks'; | |||
import { CurrentUser } from '../../../../../types/users'; | |||
import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta'; | |||
jest.mock('../../../../../api/ce'); | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => handler.reset()); | |||
it('should render correctly for a branch with warnings', async () => { | |||
const user = userEvent.setup(); | |||
jest.mocked(getAnalysisStatus).mockResolvedValue({ | |||
component: { | |||
warnings: [{ dismissable: false, key: 'key', message: 'bar' }], | |||
key: 'compkey', | |||
name: 'me', | |||
}, | |||
}); | |||
renderHeaderMeta({}, undefined, 'branch=normal-branch&id=my-project'); | |||
expect(await screen.findByText('version_x.0.0.1')).toBeInTheDocument(); | |||
expect( | |||
await screen.findByText('project_navigation.analysis_status.warnings'), | |||
).toBeInTheDocument(); | |||
await user.click(screen.getByText('project_navigation.analysis_status.details_link')); | |||
expect(screen.getByRole('heading', { name: 'warnings' })).toBeInTheDocument(); | |||
}); | |||
it('should handle a branch with missing version and no warnings', () => { | |||
jest.mocked(getAnalysisStatus).mockResolvedValue({ | |||
component: { | |||
warnings: [], | |||
key: 'compkey', | |||
name: 'me', | |||
}, | |||
}); | |||
renderHeaderMeta({ component: mockComponent({ version: undefined }) }); | |||
expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument(); | |||
expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument(); | |||
}); | |||
it('should render correctly with a failed analysis', async () => { | |||
const user = userEvent.setup(); | |||
renderHeaderMeta({ | |||
currentTask: mockTask({ | |||
status: TaskStatuses.Failed, | |||
errorMessage: 'this is the error message', | |||
}), | |||
}); | |||
expect(await screen.findByText('project_navigation.analysis_status.failed')).toBeInTheDocument(); | |||
await user.click(screen.getByText('project_navigation.analysis_status.details_link')); | |||
expect(screen.getByRole('heading', { name: 'error' })).toBeInTheDocument(); | |||
}); | |||
it('should render correctly for a pull request', async () => { | |||
renderHeaderMeta({}, undefined, 'pullRequest=01&id=my-project'); | |||
expect( | |||
await screen.findByText('branch_like_navigation.for_merge_into_x_from_y'), | |||
).toBeInTheDocument(); | |||
expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument(); | |||
}); | |||
it('should render correctly when the user is not logged in', () => { | |||
renderHeaderMeta({}, mockCurrentUser({ dismissedNotices: {} })); | |||
expect(screen.queryByText('homepage.current.is_default')).not.toBeInTheDocument(); | |||
expect(screen.queryByText('homepage.current')).not.toBeInTheDocument(); | |||
expect(screen.queryByText('homepage.check')).not.toBeInTheDocument(); | |||
}); | |||
function renderHeaderMeta( | |||
props: Partial<HeaderMetaProps> = {}, | |||
currentUser: CurrentUser = mockLoggedInUser(), | |||
params?: string, | |||
) { | |||
return renderApp('/', <HeaderMeta component={mockComponent({ version: '0.0.1' })} {...props} />, { | |||
currentUser, | |||
navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project', | |||
featureList: [Feature.BranchSupport], | |||
}); | |||
} |
@@ -19,36 +19,31 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { isPullRequest } from '../../../../../helpers/branch-like'; | |||
import { translate, translateWithParameters } from '../../../../../helpers/l10n'; | |||
import { BranchLike } from '../../../../../types/branch-like'; | |||
import { PullRequest } from '../../../../../types/branch-like'; | |||
export interface CurrentBranchLikeMergeInformationProps { | |||
currentBranchLike: BranchLike; | |||
pullRequest: PullRequest; | |||
} | |||
export function CurrentBranchLikeMergeInformation(props: CurrentBranchLikeMergeInformationProps) { | |||
const { currentBranchLike } = props; | |||
if (!isPullRequest(currentBranchLike)) { | |||
return null; | |||
} | |||
export function CurrentBranchLikeMergeInformation({ | |||
pullRequest, | |||
}: Readonly<CurrentBranchLikeMergeInformationProps>) { | |||
return ( | |||
<span | |||
className="sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-mx-1 sw-flex-shrink sw-min-w-0" | |||
className="sw-w-[400px] sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-flex-shrink sw-min-w-0" | |||
title={translateWithParameters( | |||
'branch_like_navigation.for_merge_into_x_from_y.title', | |||
currentBranchLike.target, | |||
currentBranchLike.branch, | |||
pullRequest.target, | |||
pullRequest.branch, | |||
)} | |||
> | |||
<FormattedMessage | |||
defaultMessage={translate('branch_like_navigation.for_merge_into_x_from_y')} | |||
id="branch_like_navigation.for_merge_into_x_from_y" | |||
values={{ | |||
target: <strong>{currentBranchLike.target}</strong>, | |||
branch: <strong>{currentBranchLike.branch}</strong>, | |||
target: <strong>{pullRequest.target}</strong>, | |||
branch: <strong>{pullRequest.branch}</strong>, | |||
}} | |||
/> | |||
</span> |
@@ -0,0 +1,77 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { SeparatorCircleIcon } from 'design-system'; | |||
import React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { getCurrentPage } from '../../../app/components/nav/component/utils'; | |||
import ComponentReportActions from '../../../components/controls/ComponentReportActions'; | |||
import HomePageSelect from '../../../components/controls/HomePageSelect'; | |||
import { findMeasure, formatMeasure } from '../../../helpers/measures'; | |||
import { Branch } from '../../../types/branch-like'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { Component, MeasureEnhanced } from '../../../types/types'; | |||
import { HomePage } from '../../../types/users'; | |||
interface Props { | |||
component: Component; | |||
branch: Branch; | |||
measures: MeasureEnhanced[]; | |||
} | |||
export default function BranchMetaTopBar({ branch, measures, component }: Readonly<Props>) { | |||
const intl = useIntl(); | |||
const currentPage = getCurrentPage(component, branch) as HomePage; | |||
const locMeasure = findMeasure(measures, MetricKey.lines); | |||
const leftSection = ( | |||
<h1 className="sw-flex sw-gap-2 sw-items-center sw-heading-md">{branch.name}</h1> | |||
); | |||
const rightSection = ( | |||
<div className="sw-flex sw-gap-2 sw-items-center"> | |||
{locMeasure && ( | |||
<> | |||
<div className="sw-flex sw-items-center sw-gap-1"> | |||
<strong>{formatMeasure(locMeasure.value, MetricType.ShortInteger)}</strong> | |||
{intl.formatMessage({ id: 'metric.ncloc.name' })} | |||
</div> | |||
<SeparatorCircleIcon /> | |||
</> | |||
)} | |||
{component.version && ( | |||
<> | |||
<div className="sw-flex sw-items-center sw-gap-1"> | |||
{intl.formatMessage({ id: 'version_x' }, { '0': <strong>{component.version}</strong> })} | |||
</div> | |||
<SeparatorCircleIcon /> | |||
</> | |||
)} | |||
<HomePageSelect currentPage={currentPage} type="button" /> | |||
<ComponentReportActions component={component} branch={branch} /> | |||
</div> | |||
); | |||
return ( | |||
<div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm sw-mb-2"> | |||
{leftSection} | |||
{rightSection} | |||
</div> | |||
); | |||
} |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { LargeCenteredLayout, PageContentFontWrapper } from 'design-system'; | |||
import { BasicSeparator, LargeCenteredLayout, PageContentFontWrapper } from 'design-system'; | |||
import * as React from 'react'; | |||
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; | |||
import { useLocation } from '../../../components/hoc/withRouter'; | |||
@@ -30,9 +30,11 @@ import { ComponentQualifier } from '../../../types/component'; | |||
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types'; | |||
import { AnalysisStatus } from '../components/AnalysisStatus'; | |||
import { MeasuresTabs } from '../utils'; | |||
import AcceptedIssuesPanel from './AcceptedIssuesPanel'; | |||
import ActivityPanel from './ActivityPanel'; | |||
import BranchMetaTopBar from './BranchMetaTopBar'; | |||
import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif'; | |||
import MeasuresPanel from './MeasuresPanel'; | |||
import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode'; | |||
@@ -115,66 +117,74 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp | |||
{projectIsEmpty ? ( | |||
<NoCodeWarning branchLike={branch} component={component} measures={measures} /> | |||
) : ( | |||
<div className="sw-flex"> | |||
<div className="sw-w-1/3 sw-mr-12 sw-pt-6"> | |||
<QualityGatePanel | |||
component={component} | |||
loading={loadingStatus} | |||
qgStatuses={qgStatuses} | |||
qualityGate={qualityGate} | |||
/> | |||
</div> | |||
<div className="sw-flex-1"> | |||
<div className="sw-flex sw-flex-col sw-pt-6"> | |||
<TabsPanel | |||
analyses={analyses} | |||
appLeak={appLeak} | |||
branch={branch} | |||
<div> | |||
{branch && ( | |||
<> | |||
<BranchMetaTopBar branch={branch} component={component} measures={measures} /> | |||
<BasicSeparator /> | |||
</> | |||
)} | |||
<AnalysisStatus className="sw-mt-6" component={component} /> | |||
<div className="sw-flex"> | |||
<div className="sw-w-1/3 sw-mr-12 sw-pt-6"> | |||
<QualityGatePanel | |||
component={component} | |||
loading={loadingStatus} | |||
period={period} | |||
qgStatuses={qgStatuses} | |||
isNewCode={isNewCodeTab} | |||
onTabSelect={selectTab} | |||
> | |||
{!hasNewCodeMeasures && isNewCodeTab ? ( | |||
<MeasuresPanelNoNewCode | |||
branch={branch} | |||
component={component} | |||
period={period} | |||
/> | |||
) : ( | |||
<> | |||
<MeasuresPanel | |||
branch={branch} | |||
component={component} | |||
measures={measures} | |||
isNewCode={isNewCodeTab} | |||
/> | |||
qualityGate={qualityGate} | |||
/> | |||
</div> | |||
<AcceptedIssuesPanel | |||
<div className="sw-flex-1"> | |||
<div className="sw-flex sw-flex-col sw-pt-6"> | |||
<TabsPanel | |||
analyses={analyses} | |||
appLeak={appLeak} | |||
component={component} | |||
loading={loadingStatus} | |||
period={period} | |||
qgStatuses={qgStatuses} | |||
isNewCode={isNewCodeTab} | |||
onTabSelect={selectTab} | |||
> | |||
{!hasNewCodeMeasures && isNewCodeTab ? ( | |||
<MeasuresPanelNoNewCode | |||
branch={branch} | |||
component={component} | |||
measures={measures} | |||
isNewCode={isNewCodeTab} | |||
loading={loadingStatus} | |||
period={period} | |||
/> | |||
</> | |||
)} | |||
</TabsPanel> | |||
) : ( | |||
<> | |||
<MeasuresPanel | |||
branch={branch} | |||
component={component} | |||
measures={measures} | |||
isNewCode={isNewCodeTab} | |||
/> | |||
<ActivityPanel | |||
analyses={analyses} | |||
branchLike={branch} | |||
component={component} | |||
graph={graph} | |||
leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)} | |||
loading={loadingHistory} | |||
measuresHistory={measuresHistory} | |||
metrics={metrics} | |||
onGraphChange={onGraphChange} | |||
/> | |||
<AcceptedIssuesPanel | |||
branch={branch} | |||
component={component} | |||
measures={measures} | |||
isNewCode={isNewCodeTab} | |||
loading={loadingStatus} | |||
/> | |||
</> | |||
)} | |||
</TabsPanel> | |||
<ActivityPanel | |||
analyses={analyses} | |||
branchLike={branch} | |||
component={component} | |||
graph={graph} | |||
leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)} | |||
loading={loadingHistory} | |||
measuresHistory={measuresHistory} | |||
metrics={metrics} | |||
onGraphChange={onGraphChange} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</div> |
@@ -29,11 +29,9 @@ import { | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import ComponentReportActions from '../../../components/controls/ComponentReportActions'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { ApplicationPeriod } from '../../../types/application'; | |||
import { Branch } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
@@ -45,7 +43,6 @@ import { LeakPeriodInfo } from './LeakPeriodInfo'; | |||
export interface MeasuresPanelProps { | |||
analyses?: Analysis[]; | |||
appLeak?: ApplicationPeriod; | |||
branch?: Branch; | |||
component: Component; | |||
loading?: boolean; | |||
period?: Period; | |||
@@ -60,7 +57,6 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) { | |||
const { | |||
analyses, | |||
appLeak, | |||
branch, | |||
component, | |||
loading, | |||
period, | |||
@@ -128,9 +124,6 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) { | |||
return ( | |||
<div data-test="overview__measures-panel"> | |||
<div className="sw-float-right -sw-mt-6"> | |||
<ComponentReportActions component={component} branch={branch} /> | |||
</div> | |||
<div className="sw-flex sw-mb-4"> | |||
<PageTitle as="h2" text={translate('overview.measures')} /> | |||
</div> |
@@ -210,8 +210,12 @@ describe('project overview', () => { | |||
); | |||
renderBranchOverview(); | |||
// Meta info | |||
expect(await screen.findByText('master')).toBeInTheDocument(); | |||
expect(screen.getByText('version-1.0')).toBeInTheDocument(); | |||
// QG panel | |||
expect(await screen.findByText('metric.level.OK')).toBeInTheDocument(); | |||
expect(screen.getByText('metric.level.OK')).toBeInTheDocument(); | |||
expect(screen.getByText('overview.passed.clean_code')).toBeInTheDocument(); | |||
expect( | |||
screen.queryByText('overview.quality_gate.conditions.cayc.warning'), | |||
@@ -540,6 +544,7 @@ function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) { | |||
breadcrumbs: [mockComponent({ key: 'foo' })], | |||
key: 'foo', | |||
name: 'Foo', | |||
version: 'version-1.0', | |||
})} | |||
{...props} | |||
/> |
@@ -21,13 +21,13 @@ import { Link } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { useLocation } from 'react-router-dom'; | |||
import { isBranch, isMainBranch, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { hasMessage, translate } from '../../../../helpers/l10n'; | |||
import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls'; | |||
import { useBranchesQuery } from '../../../../queries/branch'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { isBranch, isMainBranch, isPullRequest } from '../../../helpers/branch-like'; | |||
import { hasMessage, translate } from '../../../helpers/l10n'; | |||
import { getComponentBackgroundTaskUrl } from '../../../helpers/urls'; | |||
import { useBranchesQuery } from '../../../queries/branch'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Task } from '../../../types/tasks'; | |||
import { Component } from '../../../types/types'; | |||
interface Props { | |||
component: Component; |
@@ -18,11 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import Modal from '../../../../components/controls/Modal'; | |||
import { ResetButtonLink } from '../../../../components/controls/buttons'; | |||
import { hasMessage, translate } from '../../../../helpers/l10n'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import { ResetButtonLink } from '../../../components/controls/buttons'; | |||
import { hasMessage, translate } from '../../../helpers/l10n'; | |||
import { Task } from '../../../types/tasks'; | |||
import { Component } from '../../../types/types'; | |||
import { AnalysisErrorMessage } from './AnalysisErrorMessage'; | |||
import { AnalysisLicenseError } from './AnalysisLicenseError'; | |||
@@ -18,11 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import Link from '../../../../components/common/Link'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { AppStateContext } from '../../app-state/AppStateContext'; | |||
import { AppStateContext } from '../../../app/components/app-state/AppStateContext'; | |||
import Link from '../../../components/common/Link'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { Task } from '../../../types/tasks'; | |||
import { useLicenseIsValid } from './useLicenseIsValid'; | |||
interface Props { |
@@ -17,24 +17,25 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import { FlagMessage, Link, Spinner } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { useBranchWarningQuery } from '../../../../queries/branch'; | |||
import { Task, TaskStatuses } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { useComponent } from '../../../app/components/componentContext/withComponentContext'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { useBranchWarningQuery } from '../../../queries/branch'; | |||
import { TaskStatuses } from '../../../types/tasks'; | |||
import { Component } from '../../../types/types'; | |||
import { AnalysisErrorModal } from './AnalysisErrorModal'; | |||
import AnalysisWarningsModal from './AnalysisWarningsModal'; | |||
export interface HeaderMetaProps { | |||
currentTask?: Task; | |||
component: Component; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
className?: string; | |||
} | |||
export function AnalysisStatus(props: HeaderMetaProps) { | |||
const { component, currentTask, isInProgress, isPending } = props; | |||
const { className, component } = props; | |||
const { currentTask, isPending, isInProgress } = useComponent(); | |||
const { data: warnings, isLoading } = useBranchWarningQuery(component); | |||
const [modalIsVisible, setDisplayModal] = React.useState(false); | |||
@@ -47,7 +48,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { | |||
if (isInProgress || isPending) { | |||
return ( | |||
<div className="sw-flex sw-items-center"> | |||
<div data-test="analysis-status" className={classNames('sw-flex sw-items-center', className)}> | |||
<Spinner /> | |||
<span className="sw-ml-1"> | |||
{isInProgress | |||
@@ -61,7 +62,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { | |||
if (currentTask?.status === TaskStatuses.Failed) { | |||
return ( | |||
<> | |||
<FlagMessage variant="error"> | |||
<FlagMessage data-test="analysis-status" variant="error" className={className}> | |||
<span>{translate('project_navigation.analysis_status.failed')}</span> | |||
<Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}> | |||
{translate('project_navigation.analysis_status.details_link')} | |||
@@ -81,7 +82,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { | |||
if (!isLoading && warnings && warnings.length > 0) { | |||
return ( | |||
<> | |||
<FlagMessage variant="warning"> | |||
<FlagMessage data-test="analysis-status" variant="warning" className={className}> | |||
<span>{translate('project_navigation.analysis_status.warnings')}</span> | |||
<Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}> | |||
{translate('project_navigation.analysis_status.details_link')} |
@@ -19,13 +19,13 @@ | |||
*/ | |||
import { DangerButtonSecondary, FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { sanitizeStringRestricted } from '../../../../helpers/sanitize'; | |||
import { useDismissBranchWarningMutation } from '../../../../queries/branch'; | |||
import { TaskWarning } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { CurrentUser } from '../../../../types/users'; | |||
import withCurrentUserContext from '../../current-user/withCurrentUserContext'; | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeStringRestricted } from '../../../helpers/sanitize'; | |||
import { useDismissBranchWarningMutation } from '../../../queries/branch'; | |||
import { TaskWarning } from '../../../types/tasks'; | |||
import { Component } from '../../../types/types'; | |||
import { CurrentUser } from '../../../types/users'; | |||
interface Props { | |||
component: Component; |
@@ -1,138 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { getLeakValue } from '../../../components/measure/utils'; | |||
import DrilldownLink from '../../../components/shared/DrilldownLink'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Component, MeasureEnhanced } from '../../../types/types'; | |||
import { | |||
getMeasurementIconClass, | |||
getMeasurementLabelKeys, | |||
getMeasurementLinesMetricKey, | |||
getMeasurementMetricKey, | |||
MeasurementType, | |||
} from '../utils'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
centered?: boolean; | |||
component: Component; | |||
measures: MeasureEnhanced[]; | |||
type: MeasurementType; | |||
useDiffMetric?: boolean; | |||
} | |||
export default class MeasurementLabel extends React.Component<Props> { | |||
getLabelText = () => { | |||
const { branchLike, component, measures, type, useDiffMetric = false } = this.props; | |||
const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric); | |||
const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric); | |||
const measure = findMeasure(measures, linesMetric); | |||
if (!measure) { | |||
return translate(labelKey); | |||
} | |||
const value = useDiffMetric ? getLeakValue(measure) : measure.value; | |||
return ( | |||
<FormattedMessage | |||
defaultMessage={translate(expandedLabelKey)} | |||
id={expandedLabelKey} | |||
values={{ | |||
count: ( | |||
<DrilldownLink | |||
branchLike={branchLike} | |||
className="big" | |||
component={component.key} | |||
metric={linesMetric} | |||
> | |||
{formatMeasure(value, 'SHORT_INT')} | |||
</DrilldownLink> | |||
), | |||
}} | |||
/> | |||
); | |||
}; | |||
render() { | |||
const { branchLike, centered, component, measures, type, useDiffMetric = false } = this.props; | |||
const iconClass = getMeasurementIconClass(type); | |||
const metricKey = getMeasurementMetricKey(type, useDiffMetric); | |||
const measure = findMeasure(measures, metricKey); | |||
let value; | |||
if (measure) { | |||
value = useDiffMetric ? getLeakValue(measure) : measure.value; | |||
} | |||
if (value === undefined) { | |||
return ( | |||
<div className="display-flex-center"> | |||
<span aria-label={translate('no_data')} className="overview-measures-empty-value" /> | |||
<span className="big-spacer-left">{this.getLabelText()}</span> | |||
</div> | |||
); | |||
} | |||
const icon = React.createElement(iconClass, { size: 'big', value: Number(value) }); | |||
const formattedValue = formatMeasure(value, 'PERCENT', { | |||
decimals: 2, | |||
omitExtraDecimalZeros: true, | |||
}); | |||
const link = ( | |||
<DrilldownLink | |||
ariaLabel={translateWithParameters( | |||
'overview.see_more_details_on_x_of_y', | |||
formattedValue, | |||
localizeMetric(metricKey), | |||
)} | |||
branchLike={branchLike} | |||
className="overview-measures-value text-light" | |||
component={component.key} | |||
metric={metricKey} | |||
> | |||
{formattedValue} | |||
</DrilldownLink> | |||
); | |||
const label = this.getLabelText(); | |||
return centered ? ( | |||
<div className="display-flex-column flex-1"> | |||
<div className="display-flex-center display-flex-justify-center"> | |||
<span className="big-spacer-right">{icon}</span> | |||
{link} | |||
</div> | |||
<div className="spacer-top text-center">{label}</div> | |||
</div> | |||
) : ( | |||
<div className="display-flex-center"> | |||
<span className="big-spacer-right">{icon}</span> | |||
<div className="display-flex-column"> | |||
<span>{link}</span> | |||
<span className="spacer-top">{label}</span> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -19,11 +19,11 @@ | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { mockTask } from '../../../../../helpers/mocks/tasks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../../../types/features'; | |||
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockTask } from '../../../../helpers/mocks/tasks'; | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../../types/features'; | |||
import { AnalysisErrorMessage } from '../AnalysisErrorMessage'; | |||
const handler = new BranchesServiceMock(); |
@@ -19,13 +19,13 @@ | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { isValidLicense } from '../../../../../api/editions'; | |||
import { mockTask } from '../../../../../helpers/mocks/tasks'; | |||
import { mockAppState } from '../../../../../helpers/testMocks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { isValidLicense } from '../../../../api/editions'; | |||
import { mockTask } from '../../../../helpers/mocks/tasks'; | |||
import { mockAppState } from '../../../../helpers/testMocks'; | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import { AnalysisLicenseError } from '../AnalysisLicenseError'; | |||
jest.mock('../../../../../api/editions', () => ({ | |||
jest.mock('../../../../api/editions', () => ({ | |||
isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }), | |||
})); | |||
@@ -0,0 +1,107 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; | |||
import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock'; | |||
import { useComponent } from '../../../../app/components/componentContext/withComponentContext'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockTask, mockTaskWarning } from '../../../../helpers/mocks/tasks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { TaskStatuses } from '../../../../types/tasks'; | |||
import { AnalysisStatus } from '../AnalysisStatus'; | |||
const branchesHandler = new BranchesServiceMock(); | |||
const handler = new ComputeEngineServiceMock(); | |||
jest.mock('../../../../app/components/componentContext/withComponentContext', () => ({ | |||
useComponent: jest.fn(() => ({ | |||
isInProgress: true, | |||
isPending: false, | |||
currentTask: mockTask(), | |||
component: mockComponent(), | |||
})), | |||
})); | |||
beforeEach(() => { | |||
branchesHandler.reset(); | |||
handler.reset(); | |||
}); | |||
it('renders correctly when there is a background task in progress', () => { | |||
renderAnalysisStatus(); | |||
expect( | |||
screen.getByText('project_navigation.analysis_status.in_progress', { exact: false }), | |||
).toBeInTheDocument(); | |||
}); | |||
it('renders correctly when there is a background task pending', () => { | |||
jest.mocked(useComponent).mockReturnValue({ | |||
isInProgress: false, | |||
isPending: true, | |||
currentTask: mockTask(), | |||
onComponentChange: jest.fn(), | |||
fetchComponent: jest.fn(), | |||
}); | |||
renderAnalysisStatus(); | |||
expect( | |||
screen.getByText('project_navigation.analysis_status.pending', { exact: false }), | |||
).toBeInTheDocument(); | |||
}); | |||
it('renders correctly when there is a failing background task', () => { | |||
jest.mocked(useComponent).mockReturnValue({ | |||
isInProgress: false, | |||
isPending: false, | |||
currentTask: mockTask({ status: TaskStatuses.Failed }), | |||
onComponentChange: jest.fn(), | |||
fetchComponent: jest.fn(), | |||
}); | |||
renderAnalysisStatus(); | |||
expect( | |||
screen.getByText('project_navigation.analysis_status.failed', { exact: false }), | |||
).toBeInTheDocument(); | |||
}); | |||
it('renders correctly when there are analysis warnings', async () => { | |||
const user = userEvent.setup(); | |||
jest.mocked(useComponent).mockReturnValue({ | |||
isInProgress: false, | |||
isPending: false, | |||
currentTask: mockTask(), | |||
onComponentChange: jest.fn(), | |||
fetchComponent: jest.fn(), | |||
}); | |||
handler.setTaskWarnings([mockTaskWarning({ message: 'warning 1' })]); | |||
renderAnalysisStatus(); | |||
await user.click(await screen.findByText('project_navigation.analysis_status.details_link')); | |||
expect(screen.getByText('warning 1')).toBeInTheDocument(); | |||
await user.click(screen.getByText('close')); | |||
expect(screen.queryByText('warning 1')).not.toBeInTheDocument(); | |||
}); | |||
function renderAnalysisStatus(overrides: Partial<Parameters<typeof AnalysisStatus>[0]> = {}) { | |||
return renderComponent( | |||
<AnalysisStatus component={mockComponent()} {...overrides} />, | |||
'/?id=my-project', | |||
); | |||
} |
@@ -19,14 +19,14 @@ | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { AnalysisWarningsModal } from '../../../app/components/nav/component/AnalysisWarningsModal'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { mockTaskWarning } from '../../../helpers/mocks/tasks'; | |||
import { mockCurrentUser } from '../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import { ComponentPropsType } from '../../../helpers/testUtils'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockTaskWarning } from '../../../../helpers/mocks/tasks'; | |||
import { mockCurrentUser } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { AnalysisWarningsModal } from '../AnalysisWarningsModal'; | |||
jest.mock('../../../api/ce', () => ({ | |||
jest.mock('../../../../api/ce', () => ({ | |||
dismissAnalysisWarning: jest.fn().mockResolvedValue(null), | |||
getTask: jest.fn().mockResolvedValue({ | |||
warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n third line'], |
@@ -1,113 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { MeasurementType } from '../../utils'; | |||
import MeasurementLabel from '../MeasurementLabel'; | |||
it('should render correctly for coverage', async () => { | |||
renderMeasurementLabel(); | |||
expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument(); | |||
renderMeasurementLabel({ | |||
measures: [ | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) }), | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.lines_to_cover }) }), | |||
], | |||
}); | |||
expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument(); | |||
expect(await screen.findByText('overview.coverage_on_X_lines')).toBeInTheDocument(); | |||
renderMeasurementLabel({ | |||
measures: [ | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_coverage }) }), | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_lines_to_cover }) }), | |||
], | |||
useDiffMetric: true, | |||
}); | |||
expect(screen.getByRole('link', { name: /.*new_coverage.*/ })).toBeInTheDocument(); | |||
expect(await screen.findByText('overview.coverage_on_X_lines')).toBeInTheDocument(); | |||
expect(await screen.findByText('overview.coverage_on_X_new_lines')).toBeInTheDocument(); | |||
}); | |||
it('should render correctly for duplications', async () => { | |||
renderMeasurementLabel({ | |||
measures: [ | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.duplicated_lines_density }) }), | |||
], | |||
type: MeasurementType.Duplication, | |||
}); | |||
expect( | |||
screen.getByRole('link', { | |||
name: 'overview.see_more_details_on_x_of_y.1.0%.metric.duplicated_lines_density.name', | |||
}), | |||
).toBeInTheDocument(); | |||
expect(await screen.findByText('metric.duplicated_lines_density.short_name')).toBeInTheDocument(); | |||
renderMeasurementLabel({ | |||
measures: [ | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.duplicated_lines_density }) }), | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.ncloc }) }), | |||
], | |||
type: MeasurementType.Duplication, | |||
}); | |||
expect(await screen.findByText('metric.duplicated_lines_density.short_name')).toBeInTheDocument(); | |||
expect(await screen.findByText('overview.duplications_on_X_lines')).toBeInTheDocument(); | |||
renderMeasurementLabel({ | |||
measures: [ | |||
mockMeasureEnhanced({ | |||
metric: mockMetric({ key: MetricKey.new_duplicated_lines_density }), | |||
}), | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_lines }) }), | |||
], | |||
type: MeasurementType.Duplication, | |||
useDiffMetric: true, | |||
}); | |||
expect( | |||
screen.getByRole('link', { | |||
name: 'overview.see_more_details_on_x_of_y.1.0%.metric.new_duplicated_lines_density.name', | |||
}), | |||
).toBeInTheDocument(); | |||
expect(await screen.findByText('overview.duplications_on_X_new_lines')).toBeInTheDocument(); | |||
}); | |||
it('should render correctly with no value', async () => { | |||
renderMeasurementLabel({ measures: [] }); | |||
expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument(); | |||
}); | |||
function renderMeasurementLabel(props: Partial<MeasurementLabel['props']> = {}) { | |||
return renderComponent( | |||
<MeasurementLabel | |||
branchLike={mockPullRequest()} | |||
component={mockComponent()} | |||
measures={[mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) })]} | |||
type={MeasurementType.Coverage} | |||
{...props} | |||
/>, | |||
); | |||
} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React, { useEffect } from 'react'; | |||
import { isValidLicense } from '../../../../api/editions'; | |||
import { isValidLicense } from '../../../api/editions'; | |||
export function useLicenseIsValid(): [boolean, boolean] { | |||
const [licenseIsValid, setLicenseIsValid] = React.useState(false); |
@@ -17,56 +17,57 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { SeparatorCircleIcon } from 'design-system'; | |||
import React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import CurrentBranchLikeMergeInformation from '../../../app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import { getLeakValue } from '../../../components/measure/utils'; | |||
import { isPullRequest } from '../../../helpers/branch-like'; | |||
import { findMeasure, formatMeasure } from '../../../helpers/measures'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { PullRequest } from '../../../types/branch-like'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { MeasureEnhanced } from '../../../types/types'; | |||
interface Props { | |||
branchLike: BranchLike; | |||
pullRequest: PullRequest; | |||
measures: MeasureEnhanced[]; | |||
} | |||
export default function MetaTopBar({ branchLike, measures }: Readonly<Props>) { | |||
export default function PullRequestMetaTopBar({ pullRequest, measures }: Readonly<Props>) { | |||
const intl = useIntl(); | |||
const isPR = isPullRequest(branchLike); | |||
const leftSection = ( | |||
<div> | |||
{isPR ? ( | |||
<> | |||
<strong className="sw-body-sm-highlight sw-mr-1"> | |||
{formatMeasure( | |||
getLeakValue(findMeasure(measures, MetricKey.new_lines)), | |||
MetricType.ShortInteger, | |||
) ?? '0'} | |||
</strong> | |||
{intl.formatMessage({ id: 'metric.new_lines.name' })} | |||
</> | |||
) : null} | |||
<strong className="sw-body-sm-highlight sw-mr-1"> | |||
{formatMeasure( | |||
getLeakValue(findMeasure(measures, MetricKey.new_lines)), | |||
MetricType.ShortInteger, | |||
) || '0'} | |||
</strong> | |||
{intl.formatMessage({ id: 'metric.new_lines.name' })} | |||
</div> | |||
); | |||
const rightSection = ( | |||
<div> | |||
{branchLike.analysisDate | |||
? intl.formatMessage( | |||
<div className="sw-flex sw-items-center sw-gap-2"> | |||
<CurrentBranchLikeMergeInformation pullRequest={pullRequest} /> | |||
{pullRequest.analysisDate && ( | |||
<> | |||
<SeparatorCircleIcon /> | |||
{intl.formatMessage( | |||
{ | |||
id: 'overview.last_analysis_x', | |||
}, | |||
{ | |||
date: ( | |||
<strong className="sw-body-sm-highlight"> | |||
<DateFromNow date={branchLike.analysisDate} /> | |||
<DateFromNow date={pullRequest.analysisDate} /> | |||
</strong> | |||
), | |||
}, | |||
) | |||
: null} | |||
)} | |||
</> | |||
)} | |||
</div> | |||
); | |||
@@ -28,13 +28,14 @@ import { useComponentMeasuresWithMetricsQuery } from '../../../queries/component | |||
import { useComponentQualityGateQuery } from '../../../queries/quality-gates'; | |||
import { PullRequest } from '../../../types/branch-like'; | |||
import { Component } from '../../../types/types'; | |||
import { AnalysisStatus } from '../components/AnalysisStatus'; | |||
import BranchQualityGate from '../components/BranchQualityGate'; | |||
import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; | |||
import MetaTopBar from '../components/MetaTopBar'; | |||
import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide'; | |||
import '../styles.css'; | |||
import { PR_METRICS, Status } from '../utils'; | |||
import MeasuresCardPanel from './MeasuresCardPanel'; | |||
import PullRequestMetaTopBar from './PullRequestMetaTopBar'; | |||
import SonarLintAd from './SonarLintAd'; | |||
interface Props { | |||
@@ -97,9 +98,11 @@ export default function PullRequestOverview(props: Readonly<Readonly<Props>>) { | |||
<CenteredLayout> | |||
<PageContentFontWrapper className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12 sw-body-sm"> | |||
<div className="sw-col-start-2 sw-col-span-10"> | |||
<MetaTopBar branchLike={pullRequest} measures={measures} /> | |||
<PullRequestMetaTopBar pullRequest={pullRequest} measures={measures} /> | |||
<BasicSeparator className="sw-my-4" /> | |||
<AnalysisStatus className="sw-mb-4" component={component} /> | |||
{ignoredConditions && <IgnoredConditionWarning />} | |||
{status && ( |
@@ -17,12 +17,12 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { DiscreetInteractiveIcon, HomeFillIcon, HomeIcon } from 'design-system'; | |||
import * as React from 'react'; | |||
import { ButtonSecondary, DiscreetInteractiveIcon, HomeFillIcon, HomeIcon } from 'design-system'; | |||
import React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { setHomePage } from '../../api/users'; | |||
import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext'; | |||
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { isSameHomePage } from '../../helpers/users'; | |||
import { HomePage, isLoggedIn } from '../../types/users'; | |||
import Tooltip from './Tooltip'; | |||
@@ -31,55 +31,62 @@ interface Props | |||
extends Pick<CurrentUserContextInterface, 'currentUser' | 'updateCurrentUserHomepage'> { | |||
className?: string; | |||
currentPage: HomePage; | |||
type?: 'button' | 'icon'; | |||
} | |||
export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' }; | |||
export class HomePageSelect extends React.PureComponent<Props> { | |||
async setCurrentUserHomepage(homepage: HomePage) { | |||
const { currentUser } = this.props; | |||
export function HomePageSelect(props: Readonly<Props>) { | |||
const { currentPage, className, type = 'icon', currentUser, updateCurrentUserHomepage } = props; | |||
const intl = useIntl(); | |||
if (!isLoggedIn(currentUser)) { | |||
return null; | |||
} | |||
const isChecked = | |||
currentUser.homepage !== undefined && isSameHomePage(currentUser.homepage, currentPage); | |||
const isDefault = isChecked && isSameHomePage(currentPage, DEFAULT_HOMEPAGE); | |||
const setCurrentUserHomepage = async (homepage: HomePage) => { | |||
if (isLoggedIn(currentUser)) { | |||
await setHomePage(homepage); | |||
this.props.updateCurrentUserHomepage(homepage); | |||
updateCurrentUserHomepage(homepage); | |||
} | |||
} | |||
handleClick = () => { | |||
this.setCurrentUserHomepage(this.props.currentPage); | |||
}; | |||
handleReset = () => { | |||
this.setCurrentUserHomepage(DEFAULT_HOMEPAGE); | |||
}; | |||
render() { | |||
const { className, currentPage, currentUser } = this.props; | |||
const tooltip = isChecked | |||
? intl.formatMessage({ id: isDefault ? 'homepage.current.is_default' : 'homepage.current' }) | |||
: intl.formatMessage({ id: 'homepage.check' }); | |||
if (!isLoggedIn(currentUser)) { | |||
return null; | |||
} | |||
const handleClick = () => setCurrentUserHomepage?.(isChecked ? DEFAULT_HOMEPAGE : currentPage); | |||
const { homepage } = currentUser; | |||
const isChecked = homepage !== undefined && isSameHomePage(homepage, currentPage); | |||
const isDefault = isChecked && isSameHomePage(currentPage, DEFAULT_HOMEPAGE); | |||
const tooltip = isChecked | |||
? translate(isDefault ? 'homepage.current.is_default' : 'homepage.current') | |||
: translate('homepage.check'); | |||
const Icon = isChecked ? HomeFillIcon : HomeIcon; | |||
return ( | |||
<Tooltip overlay={tooltip}> | |||
return ( | |||
<Tooltip overlay={tooltip}> | |||
{type === 'icon' ? ( | |||
<DiscreetInteractiveIcon | |||
aria-label={tooltip} | |||
className={className} | |||
disabled={isDefault} | |||
Icon={isChecked ? HomeFillIcon : HomeIcon} | |||
onClick={isChecked ? this.handleReset : this.handleClick} | |||
Icon={Icon} | |||
onClick={handleClick} | |||
/> | |||
</Tooltip> | |||
); | |||
} | |||
) : ( | |||
<ButtonSecondary | |||
aria-label={tooltip} | |||
icon={<Icon />} | |||
className={className} | |||
disabled={isDefault} | |||
onClick={handleClick} | |||
> | |||
{intl.formatMessage({ id: 'overview.set_as_homepage' })} | |||
</ButtonSecondary> | |||
)} | |||
</Tooltip> | |||
); | |||
} | |||
export default withCurrentUserContext(HomePageSelect); |
@@ -23,6 +23,7 @@ import * as React from 'react'; | |||
import { setHomePage } from '../../../api/users'; | |||
import { mockLoggedInUser } from '../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import { FCProps } from '../../../types/misc'; | |||
import { DEFAULT_HOMEPAGE, HomePageSelect } from '../HomePageSelect'; | |||
jest.mock('../../../api/users', () => ({ | |||
@@ -56,7 +57,7 @@ it('renders correctly if user is on the homepage', async () => { | |||
expect(button).toHaveFocus(); | |||
}); | |||
function renderHomePageSelect(props: Partial<HomePageSelect['props']> = {}) { | |||
function renderHomePageSelect(props: Partial<FCProps<typeof HomePageSelect>> = {}) { | |||
return renderComponent( | |||
<HomePageSelect | |||
currentPage={{ type: 'MY_PROJECTS' }} |
@@ -28,6 +28,7 @@ import { | |||
Title, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import { AnalysisStatus } from '../../apps/overview/components/AnalysisStatus'; | |||
import { isMainBranch } from '../../helpers/branch-like'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { getBaseUrl } from '../../helpers/system'; | |||
@@ -129,12 +130,15 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender | |||
return ( | |||
<div className="sw-body-sm"> | |||
<AnalysisStatus component={component} className="sw-mb-4 sw-w-max" /> | |||
{selectedTutorial === undefined && ( | |||
<div className="sw-flex sw-flex-col"> | |||
<Title className="sw-mb-6 sw-heading-lg"> | |||
{translate('onboarding.tutorial.page.title')} | |||
</Title> | |||
<LightPrimary>{translate('onboarding.tutorial.page.description')}</LightPrimary> | |||
<SubTitle className="sw-mt-12 sw-mb-4 sw-heading-md"> | |||
{translate('onboarding.tutorial.choose_method')} | |||
</SubTitle> |
@@ -103,7 +103,11 @@ export function renderAppWithAdminContext( | |||
export function renderComponent( | |||
component: React.ReactElement, | |||
pathname = '/', | |||
{ appState = mockAppState(), featureList = [] }: RenderContext = {}, | |||
{ | |||
appState = mockAppState(), | |||
featureList = [], | |||
currentUser = mockCurrentUser(), | |||
}: RenderContext = {}, | |||
) { | |||
function Wrapper({ children }: { children: React.ReactElement }) { | |||
const queryClient = new QueryClient(); | |||
@@ -113,13 +117,15 @@ export function renderComponent( | |||
<QueryClientProvider client={queryClient}> | |||
<HelmetProvider> | |||
<AvailableFeaturesContext.Provider value={featureList}> | |||
<AppStateContextProvider appState={appState}> | |||
<MemoryRouter initialEntries={[pathname]}> | |||
<Routes> | |||
<Route path="*" element={children} /> | |||
</Routes> | |||
</MemoryRouter> | |||
</AppStateContextProvider> | |||
<CurrentUserContextProvider currentUser={currentUser}> | |||
<AppStateContextProvider appState={appState}> | |||
<MemoryRouter initialEntries={[pathname]}> | |||
<Routes> | |||
<Route path="*" element={children} /> | |||
</Routes> | |||
</MemoryRouter> | |||
</AppStateContextProvider> | |||
</CurrentUserContextProvider> | |||
</AvailableFeaturesContext.Provider> | |||
</HelmetProvider> | |||
</QueryClientProvider> |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Task } from './tasks'; | |||
import { Component, LightComponent } from './types'; | |||
export enum Visibility { | |||
@@ -96,6 +97,7 @@ export function isView( | |||
export interface ComponentContextShape { | |||
component?: Component; | |||
currentTask?: Task; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
onComponentChange: (changes: Partial<Component>) => void; |
@@ -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} |