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,
export default class ComputeEngineServiceMock {
tasks: Task[];
+ taskWarnings: TaskWarning[] = [];
workers = { ...DEFAULT_WORKERS };
constructor() {
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);
}
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);
reset() {
this.tasks = cloneDeep(DEFAULT_TASKS);
+ this.taskWarnings = [];
this.workers = { ...DEFAULT_WORKERS };
}
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:
createPortal(
<ComponentNav
component={component}
- currentTask={currentTask}
isInProgress={isInProgress}
isPending={isPending}
projectBindingErrors={projectBindingErrors}
}
};
}
+
+export function useComponent() {
+ return React.useContext(ComponentContext);
+}
+++ /dev/null
-/*
- * 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 = (
- <Link onClick={props.onLeave} to={backgroundTaskUrl}>
- {translate('background_tasks.page')}
- </Link>
- );
- }
- }
-
- return (
- <FormattedMessage
- defaultMessage={translate(messageKey)}
- id={messageKey}
- values={{ branch, url, stacktrace, type }}
- />
- );
-}
+++ /dev/null
-/*
- * 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 (
- <Modal contentLabel={header} onRequestClose={props.onClose}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
-
- <div className="modal-body modal-container">
- {licenseError ? (
- <AnalysisLicenseError currentTask={currentTask} />
- ) : (
- <AnalysisErrorMessage
- component={component}
- currentTask={currentTask}
- onLeave={props.onClose}
- />
- )}
- </div>
-
- <footer className="modal-foot">
- <ResetButtonLink onClick={props.onClose}>{translate('close')}</ResetButtonLink>
- </footer>
- </Modal>
- );
-}
+++ /dev/null
-/*
- * 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 (
- <>
- <span className="little-spacer-right">{currentTask.errorMessage}</span>
- {appState.canAdmin ? (
- <Link to="/admin/extension/license/app">
- {translate('license.component_navigation.button', currentTask.errorType)}.
- </Link>
- ) : (
- translate('please_contact_administrator')
- )}
- </>
- );
-}
+++ /dev/null
-/*
- * 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 (
- <div className="sw-flex sw-items-center">
- <Spinner />
- <span className="sw-ml-1">
- {isInProgress
- ? translate('project_navigation.analysis_status.in_progress')
- : translate('project_navigation.analysis_status.pending')}
- </span>
- </div>
- );
- }
-
- if (currentTask?.status === TaskStatuses.Failed) {
- return (
- <>
- <FlagMessage variant="error">
- <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')}
- </Link>
- </FlagMessage>
- {modalIsVisible && (
- <AnalysisErrorModal
- component={component}
- currentTask={currentTask}
- onClose={closeModal}
- />
- )}
- </>
- );
- }
-
- if (!isLoading && warnings && warnings.length > 0) {
- return (
- <>
- <FlagMessage variant="warning">
- <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')}
- </Link>
- </FlagMessage>
- {modalIsVisible && (
- <AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
- )}
- </>
- );
- }
-
- return null;
-}
+++ /dev/null
-/*
- * 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 }) => (
- <React.Fragment key={key}>
- <div className="sw-flex sw-items-center sw-mt-2">
- <FlagMessage variant="warning">
- <HtmlFormatter>
- <span
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
- }}
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div>
- {dismissable && currentUser.isLoggedIn && (
- <div className="sw-mt-4">
- <DangerButtonSecondary
- disabled={Boolean(isLoading)}
- onClick={() => {
- handleDismissMessage(key);
- }}
- >
- {translate('dismiss_permanently')}
- </DangerButtonSecondary>
-
- <Spinner className="sw-ml-2" loading={isLoading && variables?.key === key} />
- </div>
- )}
- </div>
- </React.Fragment>
- ))}
- </>
- );
-
- return (
- <Modal
- headerTitle={translate('warnings')}
- onClose={props.onClose}
- body={body}
- primaryButton={null}
- secondaryButtonLabel={translate('close')}
- />
- );
-}
-
-export default withCurrentUserContext(AnalysisWarningsModal);
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, {
} 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;
<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>
+++ /dev/null
-/*
- * 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);
+++ /dev/null
-/*
- * 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<Parameters<typeof AnalysisErrorMessage>[0]> = {},
- location = '/',
- params?: string,
-) {
- return renderApp(
- location,
- <AnalysisErrorMessage
- component={mockComponent()}
- currentTask={mockTask()}
- onLeave={jest.fn()}
- {...overrides}
- />,
- { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] },
- );
-}
+++ /dev/null
-/*
- * 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<Parameters<typeof AnalysisLicenseError>[0]> = {},
- canAdmin = false,
-) {
- return renderApp('/', <AnalysisLicenseError currentTask={mockTask()} {...overrides} />, {
- appState: mockAppState({ canAdmin }),
- });
-}
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(),
+++ /dev/null
-/*
- * 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],
- });
-}
*/
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>
+++ /dev/null
-/*
- * 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];
-}
--- /dev/null
+/*
+ * 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>
+ );
+}
* 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';
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';
{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>
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';
export interface MeasuresPanelProps {
analyses?: Analysis[];
appLeak?: ApplicationPeriod;
- branch?: Branch;
component: Component;
loading?: boolean;
period?: Period;
const {
analyses,
appLeak,
- branch,
component,
loading,
period,
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>
);
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'),
breadcrumbs: [mockComponent({ key: 'foo' })],
key: 'foo',
name: 'Foo',
+ version: 'version-1.0',
})}
{...props}
/>
--- /dev/null
+/*
+ * 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 = (
+ <Link onClick={props.onLeave} to={backgroundTaskUrl}>
+ {translate('background_tasks.page')}
+ </Link>
+ );
+ }
+ }
+
+ return (
+ <FormattedMessage
+ defaultMessage={translate(messageKey)}
+ id={messageKey}
+ values={{ branch, url, stacktrace, type }}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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 (
+ <Modal contentLabel={header} onRequestClose={props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body modal-container">
+ {licenseError ? (
+ <AnalysisLicenseError currentTask={currentTask} />
+ ) : (
+ <AnalysisErrorMessage
+ component={component}
+ currentTask={currentTask}
+ onLeave={props.onClose}
+ />
+ )}
+ </div>
+
+ <footer className="modal-foot">
+ <ResetButtonLink onClick={props.onClose}>{translate('close')}</ResetButtonLink>
+ </footer>
+ </Modal>
+ );
+}
--- /dev/null
+/*
+ * 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 (
+ <>
+ <span className="little-spacer-right">{currentTask.errorMessage}</span>
+ {appState.canAdmin ? (
+ <Link to="/admin/extension/license/app">
+ {translate('license.component_navigation.button', currentTask.errorType)}.
+ </Link>
+ ) : (
+ translate('please_contact_administrator')
+ )}
+ </>
+ );
+}
--- /dev/null
+/*
+ * 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 (
+ <div data-test="analysis-status" className={classNames('sw-flex sw-items-center', className)}>
+ <Spinner />
+ <span className="sw-ml-1">
+ {isInProgress
+ ? translate('project_navigation.analysis_status.in_progress')
+ : translate('project_navigation.analysis_status.pending')}
+ </span>
+ </div>
+ );
+ }
+
+ if (currentTask?.status === TaskStatuses.Failed) {
+ return (
+ <>
+ <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')}
+ </Link>
+ </FlagMessage>
+ {modalIsVisible && (
+ <AnalysisErrorModal
+ component={component}
+ currentTask={currentTask}
+ onClose={closeModal}
+ />
+ )}
+ </>
+ );
+ }
+
+ if (!isLoading && warnings && warnings.length > 0) {
+ return (
+ <>
+ <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')}
+ </Link>
+ </FlagMessage>
+ {modalIsVisible && (
+ <AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
+ )}
+ </>
+ );
+ }
+
+ return null;
+}
--- /dev/null
+/*
+ * 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 }) => (
+ <React.Fragment key={key}>
+ <div className="sw-flex sw-items-center sw-mt-2">
+ <FlagMessage variant="warning">
+ <HtmlFormatter>
+ <span
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{
+ __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
+ }}
+ />
+ </HtmlFormatter>
+ </FlagMessage>
+ </div>
+ <div>
+ {dismissable && currentUser.isLoggedIn && (
+ <div className="sw-mt-4">
+ <DangerButtonSecondary
+ disabled={Boolean(isLoading)}
+ onClick={() => {
+ handleDismissMessage(key);
+ }}
+ >
+ {translate('dismiss_permanently')}
+ </DangerButtonSecondary>
+
+ <Spinner className="sw-ml-2" loading={isLoading && variables?.key === key} />
+ </div>
+ )}
+ </div>
+ </React.Fragment>
+ ))}
+ </>
+ );
+
+ return (
+ <Modal
+ headerTitle={translate('warnings')}
+ onClose={props.onClose}
+ body={body}
+ primaryButton={null}
+ secondaryButtonLabel={translate('close')}
+ />
+ );
+}
+
+export default withCurrentUserContext(AnalysisWarningsModal);
+++ /dev/null
-/*
- * 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>
- );
- }
-}
+++ /dev/null
-/*
- * 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<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}
- </div>
- );
- const rightSection = (
- <div>
- {branchLike.analysisDate
- ? intl.formatMessage(
- {
- id: 'overview.last_analysis_x',
- },
- {
- date: (
- <strong className="sw-body-sm-highlight">
- <DateFromNow date={branchLike.analysisDate} />
- </strong>
- ),
- },
- )
- : null}
- </div>
- );
-
- return (
- <div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm">
- {leftSection}
- {rightSection}
- </div>
- );
-}
--- /dev/null
+/*
+ * 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<Parameters<typeof AnalysisErrorMessage>[0]> = {},
+ location = '/',
+ params?: string,
+) {
+ return renderApp(
+ location,
+ <AnalysisErrorMessage
+ component={mockComponent()}
+ currentTask={mockTask()}
+ onLeave={jest.fn()}
+ {...overrides}
+ />,
+ { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] },
+ );
+}
--- /dev/null
+/*
+ * 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<Parameters<typeof AnalysisLicenseError>[0]> = {},
+ canAdmin = false,
+) {
+ return renderApp('/', <AnalysisLicenseError currentTask={mockTask()} {...overrides} />, {
+ appState: mockAppState({ canAdmin }),
+ });
+}
--- /dev/null
+/*
+ * 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',
+ );
+}
--- /dev/null
+/*
+ * 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<ComponentPropsType<typeof AnalysisWarningsModal>> = {},
+) {
+ return renderComponent(
+ <AnalysisWarningsModal
+ component={mockComponent()}
+ currentUser={mockCurrentUser({ isLoggedIn: true })}
+ onClose={jest.fn()}
+ warnings={[
+ mockTaskWarning({ message: 'warning 1' }),
+ mockTaskWarning({ message: 'warning 2' }),
+ ]}
+ {...props}
+ />,
+ );
+}
+++ /dev/null
-/*
- * 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}
- />,
- );
-}
--- /dev/null
+/*
+ * 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];
+}
--- /dev/null
+/*
+ * 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<Props>) {
+ const intl = useIntl();
+
+ const leftSection = (
+ <div>
+ <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 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={pullRequest.analysisDate} />
+ </strong>
+ ),
+ },
+ )}
+ </>
+ )}
+ </div>
+ );
+
+ return (
+ <div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm">
+ {leftSection}
+ {rightSection}
+ </div>
+ );
+}
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 {
<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 && (
+++ /dev/null
-/*
- * 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<ComponentPropsType<typeof AnalysisWarningsModal>> = {},
-) {
- return renderComponent(
- <AnalysisWarningsModal
- component={mockComponent()}
- currentUser={mockCurrentUser({ isLoggedIn: true })}
- onClose={jest.fn()}
- warnings={[
- mockTaskWarning({ message: 'warning 1' }),
- mockTaskWarning({ message: 'warning 2' }),
- ]}
- {...props}
- />,
- );
-}
* 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';
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);
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', () => ({
expect(button).toHaveFocus();
});
-function renderHomePageSelect(props: Partial<HomePageSelect['props']> = {}) {
+function renderHomePageSelect(props: Partial<FCProps<typeof HomePageSelect>> = {}) {
return renderComponent(
<HomePageSelect
currentPage={{ type: 'MY_PROJECTS' }}
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';
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>
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();
<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>
* 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 {
export interface ComponentContextShape {
component?: Component;
+ currentTask?: Task;
isInProgress?: boolean;
isPending?: boolean;
onComponentChange: (changes: Partial<Component>) => void;
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}