--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
+import classNames from 'classnames';
+import * as React from 'react';
+import tw from 'twin.macro';
+import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
+import { ThemeColors } from '../types/theme';
+import { FlagErrorIcon, FlagInfoIcon, FlagSuccessIcon, FlagWarningIcon } from './icons';
+
+export type Variant = 'error' | 'warning' | 'success' | 'info';
+
+interface Props {
+ ariaLabel: string;
+ variant: Variant;
+}
+
+interface VariantInformation {
+ backGroundColor: ThemeColors;
+ borderColor: ThemeColors;
+ icon: JSX.Element;
+ role: string;
+}
+
+function getVariantInfo(variant: Variant): VariantInformation {
+ const variantList: Record<Variant, VariantInformation> = {
+ error: {
+ icon: <FlagErrorIcon />,
+ borderColor: 'errorBorder',
+ backGroundColor: 'errorBackground',
+ role: 'alert',
+ },
+ warning: {
+ icon: <FlagWarningIcon />,
+ borderColor: 'warningBorder',
+ backGroundColor: 'warningBackground',
+ role: 'alert',
+ },
+ success: {
+ icon: <FlagSuccessIcon />,
+ borderColor: 'successBorder',
+ backGroundColor: 'successBackground',
+ role: 'status',
+ },
+ info: {
+ icon: <FlagInfoIcon />,
+ borderColor: 'infoBorder',
+ backGroundColor: 'infoBackground',
+ role: 'status',
+ },
+ };
+
+ return variantList[variant];
+}
+
+export function FlagMessage(props: Props & React.HTMLAttributes<HTMLDivElement>) {
+ const { ariaLabel, className, variant, ...domProps } = props;
+ const variantInfo = getVariantInfo(variant);
+
+ return (
+ <StyledFlag
+ aria-label={ariaLabel}
+ className={classNames('alert', className)}
+ role={variantInfo.role}
+ variantInfo={variantInfo}
+ {...domProps}
+ >
+ <StyledFlagInner>
+ <StyledFlagIcon variantInfo={variantInfo}>{variantInfo.icon}</StyledFlagIcon>
+ <StyledFlagContent>{props.children}</StyledFlagContent>
+ </StyledFlagInner>
+ </StyledFlag>
+ );
+}
+
+export const StyledFlag = styled.div<{
+ variantInfo: VariantInformation;
+}>`
+ ${tw`sw-inline-flex`}
+ ${tw`sw-min-h-10`}
+ ${tw`sw-rounded-1`}
+ border: ${({ variantInfo }) => themeBorder('default', variantInfo.borderColor)};
+ background-color: ${themeColor('flagMessageBackground')};
+`;
+
+const StyledFlagInner = styled.div`
+ ${tw`sw-flex sw-items-stretch`}
+ ${tw`sw-box-border`}
+`;
+
+const StyledFlagIcon = styled.div<{ variantInfo: VariantInformation }>`
+ ${tw`sw-flex sw-justify-center sw-items-center`}
+ ${tw`sw-rounded-l-1`}
+ ${tw`sw-px-3`}
+ background-color: ${({ variantInfo }) => themeColor(variantInfo.backGroundColor)};
+`;
+
+const StyledFlagContent = styled.div`
+ ${tw`sw-flex sw-flex-auto sw-items-center`}
+ ${tw`sw-overflow-auto`}
+ ${tw`sw-text-left`}
+ ${tw`sw-mx-3 sw-my-2`}
+ ${tw`sw-body-sm`}
+ color: ${themeContrast('flagMessageBackground')};
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { render } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { FlagMessage, Variant } from '../FlagMessage';
+
+it.each([
+ ['error', 'alert', '1px solid rgb(249,112,102)'],
+ ['warning', 'alert', '1px solid rgb(248,205,92)'],
+ ['success', 'status', '1px solid rgb(50,213,131)'],
+ ['info', 'status', '1px solid rgb(110,185,228)'],
+])('should render properly for "%s" variant', (variant: Variant, expectedRole, color) => {
+ renderFlagMessage({ variant });
+
+ const item = screen.getByRole(expectedRole);
+ expect(item).toBeInTheDocument();
+ expect(item).toHaveStyle({ border: color });
+});
+
+function renderFlagMessage(props: Partial<FCProps<typeof FlagMessage>> = {}) {
+ return render(
+ <FlagMessage ariaLabel="label" variant="error" {...props}>
+ This is an error!
+ </FlagMessage>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import { themeColor } from '../../helpers/theme';
+import { CustomIcon, IconProps } from './Icon';
+
+export function FlagErrorIcon({ fill = 'iconError', ...iconProps }: IconProps) {
+ const theme = useTheme();
+ return (
+ <CustomIcon {...iconProps}>
+ <path
+ d="M7.364 1.707a1 1 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414 0L1.707 8.778a1 1 0 0 1 0-1.414l5.657-5.657ZM7 5a1 1 0 0 1 2 0v3a1 1 0 1 1-2 0V5Zm1 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"
+ style={{ fill: themeColor(fill)({ theme }) }}
+ />
+ </CustomIcon>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import { themeColor } from '../../helpers/theme';
+import { CustomIcon, IconProps } from './Icon';
+
+export function FlagInfoIcon({ fill = 'iconInfo', ...iconProps }: IconProps) {
+ const theme = useTheme();
+ return (
+ <CustomIcon {...iconProps}>
+ <path
+ d="M14 8A6 6 0 1 1 2 8a6 6 0 0 1 12 0Zm-5 3a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v3ZM8 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
+ style={{ fill: themeColor(fill)({ theme }) }}
+ />
+ </CustomIcon>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import { themeColor } from '../../helpers/theme';
+import { CustomIcon, IconProps } from './Icon';
+
+export function FlagSuccessIcon({ fill = 'iconSuccess', ...iconProps }: IconProps) {
+ const theme = useTheme();
+ return (
+ <CustomIcon {...iconProps}>
+ <path
+ d="M8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Zm3.207-6.793a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l3.5-3.5Z"
+ style={{ fill: themeColor(fill)({ theme }) }}
+ />
+ </CustomIcon>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import { themeColor } from '../../helpers/theme';
+import { CustomIcon, IconProps } from './Icon';
+
+export function FlagWarningIcon({ fill = 'iconWarning', ...iconProps }: IconProps) {
+ const theme = useTheme();
+ return (
+ <CustomIcon {...iconProps}>
+ <path
+ d="M14.41 12.55a1 1 0 0 1-.893 1.45H2.625a1 1 0 0 1-.892-1.45L7.178 1.766a1 1 0 0 1 1.786 0l5.445 10.782ZM7 6a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0V6Zm1 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"
+ style={{ fill: themeColor(fill)({ theme }) }}
+ />
+ </CustomIcon>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import { themeColor } from '../../helpers/theme';
+import { CustomIcon, IconProps } from './Icon';
+
+export default function HomeFillIcon({ fill = 'iconFavorite', ...iconProps }: IconProps) {
+ const theme = useTheme();
+ const fillColor = themeColor(fill)({ theme });
+ return (
+ <CustomIcon {...iconProps}>
+ <path
+ d="M6.9995 0.280296C6.602 0.280296 6.21634 0.415622 5.906 0.664003L0.657 4.864C0.242 5.196 0 5.699 0 6.23V13.25C0 13.7141 0.184374 14.1593 0.512563 14.4874C0.840752 14.8156 1.28587 15 1.75 15H5.25C5.44891 15 5.63968 14.921 5.78033 14.7803C5.92098 14.6397 6 14.4489 6 14.25V9H8V14.25C8 14.4489 8.07902 14.6397 8.21967 14.7803C8.36032 14.921 8.55109 15 8.75 15H12.25C12.7141 15 13.1592 14.8156 13.4874 14.4874C13.8156 14.1593 14 13.7141 14 13.25V6.231C14 5.699 13.758 5.196 13.343 4.864L8.093 0.664003C7.78266 0.415622 7.397 0.280296 6.9995 0.280296Z"
+ fill={fillColor}
+ />
+ </CustomIcon>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { HomeIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export default OcticonHoc(HomeIcon);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export { default as ClockIcon } from './ClockIcon';
+export { FlagErrorIcon } from './FlagErrorIcon';
+export { FlagInfoIcon } from './FlagInfoIcon';
+export { FlagSuccessIcon } from './FlagSuccessIcon';
+export { FlagWarningIcon } from './FlagWarningIcon';
+export { default as HomeFillIcon } from './HomeFillIcon';
+export { default as HomeIcon } from './HomeIcon';
export { default as MenuHelpIcon } from './MenuHelpIcon';
export { default as MenuSearchIcon } from './MenuSearchIcon';
export { default as OpenNewTabIcon } from './OpenNewTabIcon';
export { default as Dropdown } from './Dropdown';
export * from './DropdownMenu';
export { default as DropdownToggler } from './DropdownToggler';
+export { FlagMessage } from './FlagMessage';
export * from './GenericAvatar';
export * from './icons';
export { default as InputSearch } from './InputSearch';
console.log(chalk.red.bold(data.toString()));
});
- build.on('exit', (code) => {
- if (code === 0) {
+ build.on('exit', function (code) {
+ if (code === 0 && callback) {
callback();
}
});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { hasMessage, translate } from '../../../../helpers/l10n';
+import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls';
+import { Task } from '../../../../types/tasks';
+import { Component } from '../../../../types/types';
+
+interface Props {
+ component: Component;
+ currentTask: Task;
+ currentTaskOnSameBranch?: boolean;
+ onLeave: () => void;
+}
+
+export function AnalysisErrorMessage(props: Props) {
+ const { component, currentTask, currentTaskOnSameBranch } = props;
+
+ 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-2023 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 { ButtonLink } from '../../../../components/controls/buttons';
+import Modal from '../../../../components/controls/Modal';
+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;
+ currentTaskOnSameBranch?: boolean;
+ onClose: () => void;
+}
+
+export function AnalysisErrorModal(props: Props) {
+ const { component, currentTask, currentTaskOnSameBranch } = 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}
+ currentTaskOnSameBranch={currentTaskOnSameBranch}
+ onLeave={props.onClose}
+ />
+ )}
+ </div>
+
+ <footer className="modal-foot">
+ <ButtonLink className="js-modal-close" onClick={props.onClose}>
+ {translate('close')}
+ </ButtonLink>
+ </footer>
+ </Modal>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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-2023 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 { DeferredSpinner, FlagMessage, Link } from 'design-system';
+import * as React from 'react';
+import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal';
+import { translate } from '../../../../helpers/l10n';
+import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks';
+import { Component } from '../../../../types/types';
+import { AnalysisErrorModal } from './AnalysisErrorModal';
+
+export interface HeaderMetaProps {
+ currentTask?: Task;
+ currentTaskOnSameBranch?: boolean;
+ component: Component;
+ isInProgress?: boolean;
+ isPending?: boolean;
+ onWarningDismiss: () => void;
+ warnings: TaskWarning[];
+}
+
+export function AnalysisStatus(props: HeaderMetaProps) {
+ const { component, currentTask, currentTaskOnSameBranch, isInProgress, isPending, warnings } =
+ props;
+
+ 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">
+ <DeferredSpinner timeout={0} />
+ <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 ariaLabel={translate('alert.tooltip.error')} variant="error">
+ <span>{translate('project_navigation.analysis_status.failed')}</span>
+ <Link
+ className="sw-ml-1"
+ blurAfterClick={true}
+ onClick={openModal}
+ preventDefault={true}
+ to={{}}
+ >
+ {translate('project_navigation.analysis_status.details_link')}
+ </Link>
+ </FlagMessage>
+ {modalIsVisible && (
+ <AnalysisErrorModal
+ component={component}
+ currentTask={currentTask}
+ currentTaskOnSameBranch={currentTaskOnSameBranch}
+ onClose={closeModal}
+ />
+ )}
+ </>
+ );
+ }
+
+ if (warnings.length > 0) {
+ return (
+ <>
+ <FlagMessage ariaLabel={translate('alert.tooltip.warning')} variant="warning">
+ <span>{translate('project_navigation.analysis_status.warnings')}</span>
+ <Link
+ className="sw-ml-1"
+ blurAfterClick={true}
+ onClick={openModal}
+ preventDefault={true}
+ to={{}}
+ >
+ {translate('project_navigation.analysis_status.details_link')}
+ </Link>
+ </FlagMessage>
+ {modalIsVisible && (
+ <AnalysisWarningsModal
+ componentKey={component.key}
+ onClose={closeModal}
+ taskId={currentTask?.id}
+ onWarningDismiss={props.onWarningDismiss}
+ warnings={warnings}
+ />
+ )}
+ </>
+ );
+ }
+
+ return null;
+}
} from '../../../../types/alm-settings';
import { BranchLike } from '../../../../types/branch-like';
import { ComponentQualifier } from '../../../../types/component';
-import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks';
+import { Task, TaskWarning } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
import { rawSizes } from '../../../theme';
import RecentHistory from '../../RecentHistory';
-import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif';
import Header from './Header';
import HeaderMeta from './HeaderMeta';
let contextNavHeight = contextNavHeightRaw;
- let bgTaskNotifComponent;
- if (isInProgress || isPending || (currentTask && currentTask.status === TaskStatuses.Failed)) {
- bgTaskNotifComponent = (
- <ComponentNavBgTaskNotif
- component={component}
- currentTask={currentTask}
- currentTaskOnSameBranch={currentTaskOnSameBranch}
- isInProgress={isInProgress}
- isPending={isPending}
- />
- );
- contextNavHeight += ALERT_HEIGHT;
- }
-
let prDecoNotifComponent;
if (projectBindingErrors !== undefined) {
prDecoNotifComponent = <ComponentNavProjectBindingErrorNotif component={component} />;
height={contextNavHeight}
id="context-navigation"
label={translate('qualifier', component.qualifier)}
- notif={
- <>
- {bgTaskNotifComponent}
- {prDecoNotifComponent}
- </>
- }
+ notif={<>{prDecoNotifComponent}</>}
>
<div
className={classNames('display-flex-center display-flex-space-between', {
<HeaderMeta
branchLike={currentBranchLike}
component={component}
+ currentTask={currentTask}
+ currentTaskOnSameBranch={currentTaskOnSameBranch}
+ isInProgress={isInProgress}
+ isPending={isPending}
onWarningDismiss={props.onWarningDismiss}
warnings={warnings}
/>
component={component}
isInProgress={isInProgress}
isPending={isPending}
- onToggleProjectInfo={() => setDisplayProjectInfo(!displayProjectInfo)}
+ onToggleProjectInfo={() => {
+ setDisplayProjectInfo(!displayProjectInfo);
+ }}
projectInfoDisplayed={displayProjectInfo}
/>
<InfoDrawer
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { STATUSES } from '../../../../apps/background-tasks/constants';
-import Link from '../../../../components/common/Link';
-import { Location, withRouter } from '../../../../components/hoc/withRouter';
-import { Alert } from '../../../../components/ui/Alert';
-import { hasMessage, translate } from '../../../../helpers/l10n';
-import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls';
-import { Task, TaskStatuses } from '../../../../types/tasks';
-import { Component } from '../../../../types/types';
-import ComponentNavLicenseNotif from './ComponentNavLicenseNotif';
-
-interface Props {
- component: Component;
- currentTask?: Task;
- currentTaskOnSameBranch?: boolean;
- isInProgress?: boolean;
- isPending?: boolean;
- location: Location;
-}
-
-export class ComponentNavBgTaskNotif extends React.PureComponent<Props> {
- renderMessage(messageKey: string, status?: string, branch?: string) {
- const { component, currentTask, location } = this.props;
- const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key, status);
- const canSeeBackgroundTasks =
- component.configuration && component.configuration.showBackgroundTasks;
- const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname;
-
- let type;
- if (currentTask && 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 to={backgroundTaskUrl}>{translate('background_tasks.page')}</Link>;
- }
- }
-
- return (
- <FormattedMessage
- defaultMessage={translate(messageKey)}
- id={messageKey}
- values={{ branch, url, stacktrace, type }}
- />
- );
- }
-
- render() {
- const { currentTask, currentTaskOnSameBranch, isInProgress, isPending } = this.props;
- if (isInProgress) {
- return (
- <Alert display="banner" variant="info">
- {this.renderMessage('component_navigation.status.in_progress')}
- </Alert>
- );
- } else if (isPending) {
- return (
- <Alert display="banner" variant="info">
- {this.renderMessage('component_navigation.status.pending', STATUSES.ALL)}
- </Alert>
- );
- } else if (currentTask && currentTask.status === TaskStatuses.Failed) {
- if (
- currentTask.errorType &&
- hasMessage('license.component_navigation.button', currentTask.errorType)
- ) {
- return <ComponentNavLicenseNotif currentTask={currentTask} />;
- }
- const branch =
- currentTask.branch ||
- `${currentTask.pullRequest}${
- currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : ''
- }`;
- let message;
- if (currentTaskOnSameBranch === false && branch) {
- message = this.renderMessage(
- 'component_navigation.status.failed_branch',
- undefined,
- branch
- );
- } else {
- message = this.renderMessage('component_navigation.status.failed');
- }
-
- return (
- <Alert className="null-spacer-bottom" display="banner" variant="error">
- {message}
- </Alert>
- );
- }
- return null;
- }
-}
-
-export default withRouter(ComponentNavBgTaskNotif);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { isValidLicense } from '../../../../api/editions';
-import Link from '../../../../components/common/Link';
-import { Alert } from '../../../../components/ui/Alert';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { AppState } from '../../../../types/appstate';
-import { ComponentQualifier } from '../../../../types/component';
-import { Task } from '../../../../types/tasks';
-import withAppStateContext from '../../app-state/withAppStateContext';
-
-interface Props {
- appState: AppState;
- currentTask?: Task;
-}
-
-interface State {
- isValidLicense?: boolean;
- loading: boolean;
-}
-
-export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: false };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchIsValidLicense();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchIsValidLicense = () => {
- this.setState({ loading: true });
- isValidLicense().then(
- ({ isValidLicense }) => {
- if (this.mounted) {
- this.setState({ isValidLicense, loading: false });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- };
-
- render() {
- const { currentTask, appState } = this.props;
- const { isValidLicense, loading } = this.state;
-
- if (loading || !currentTask || !currentTask.errorType) {
- return null;
- }
-
- if (isValidLicense && currentTask.errorType !== 'LICENSING_LOC') {
- return (
- <Alert display="banner" variant="error">
- {translateWithParameters(
- 'component_navigation.status.last_blocked_due_to_bad_license_X',
- translate('qualifier', currentTask.componentQualifier || ComponentQualifier.Project)
- )}
- </Alert>
- );
- }
-
- return (
- <Alert display="banner" variant="error">
- <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')
- )}
- </Alert>
- );
- }
-}
-
-export default withAppStateContext(ComponentNavLicenseNotif);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal';
-import { Alert } from '../../../../components/ui/Alert';
-import { translate } from '../../../../helpers/l10n';
-import { TaskWarning } from '../../../../types/tasks';
-
-interface Props {
- componentKey: string;
- isBranch: boolean;
- onWarningDismiss: () => void;
- warnings: TaskWarning[];
-}
-
-interface State {
- modal: boolean;
-}
-
-export default class ComponentNavWarnings extends React.PureComponent<Props, State> {
- state: State = { modal: false };
-
- handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- event.currentTarget.blur();
- this.setState({ modal: true });
- };
-
- handleCloseModal = () => {
- this.setState({ modal: false });
- };
-
- render() {
- return (
- <>
- <Alert className="js-component-analysis-warnings flex-1" display="inline" variant="warning">
- <FormattedMessage
- defaultMessage={translate('component_navigation.last_analysis_had_warnings')}
- id="component_navigation.last_analysis_had_warnings"
- values={{
- branchType: this.props.isBranch
- ? translate('branches.branch')
- : translate('branches.pr'),
- warnings: (
- <a href="#" onClick={this.handleClick}>
- <FormattedMessage
- defaultMessage={translate('component_navigation.x_warnings')}
- id="component_navigation.x_warnings"
- values={{
- warningsCount: this.props.warnings.length,
- }}
- />
- </a>
- ),
- }}
- />
- </Alert>
- {this.state.modal && (
- <AnalysisWarningsModal
- componentKey={this.props.componentKey}
- onClose={this.handleCloseModal}
- onWarningDismiss={this.props.onWarningDismiss}
- warnings={this.props.warnings}
- />
- )}
- </>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-.header-meta-warnings .alert {
- margin-bottom: 5px;
-}
-
-.header-meta-warnings .alert-content {
- padding: 6px 8px;
-}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { useIntl } from 'react-intl';
-import BranchStatus from '../../../../components/common/BranchStatus';
-import Link from '../../../../components/common/Link';
import HomePageSelect from '../../../../components/controls/HomePageSelect';
-import { formatterOption } from '../../../../components/intl/DateTimeFormatter';
import { isBranch, isPullRequest } from '../../../../helpers/branch-like';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { translateWithParameters } from '../../../../helpers/l10n';
import { BranchLike } from '../../../../types/branch-like';
-import { ComponentQualifier } from '../../../../types/component';
-import { TaskWarning } from '../../../../types/tasks';
+import { Task, TaskWarning } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
-import { CurrentUser, HomePage, isLoggedIn } from '../../../../types/users';
+import { CurrentUser, isLoggedIn } from '../../../../types/users';
import withCurrentUserContext from '../../current-user/withCurrentUserContext';
-import ComponentNavWarnings from './ComponentNavWarnings';
-import './HeaderMeta.css';
+import { AnalysisStatus } from './AnalysisStatus';
+import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation';
+import { getCurrentPage } from './utils';
export interface HeaderMetaProps {
branchLike?: BranchLike;
- currentUser: CurrentUser;
component: Component;
+ currentUser: CurrentUser;
+ currentTask?: Task;
+ currentTaskOnSameBranch?: boolean;
+ isInProgress?: boolean;
+ isPending?: boolean;
onWarningDismiss: () => void;
warnings: TaskWarning[];
}
export function HeaderMeta(props: HeaderMetaProps) {
- const { branchLike, component, currentUser, warnings } = props;
+ const {
+ branchLike,
+ component,
+ currentUser,
+ currentTask,
+ currentTaskOnSameBranch,
+ isInProgress,
+ isPending,
+ warnings,
+ } = props;
const isABranch = isBranch(branchLike);
const currentPage = getCurrentPage(component, branchLike);
- const displayVersion = component.version !== undefined && isABranch;
- const lastAnalysisDate = useIntl().formatDate(component.analysisDate, formatterOption);
return (
- <>
- <div className="display-flex-center flex-0 small">
- {warnings.length > 0 && (
- <span className="header-meta-warnings">
- <ComponentNavWarnings
- isBranch={isABranch}
- componentKey={component.key}
- onWarningDismiss={props.onWarningDismiss}
- warnings={warnings}
- />
- </span>
- )}
- {component.analysisDate && (
- <span
- title={translateWithParameters(
- 'overview.project.last_analysis.date_time',
- lastAnalysisDate
- )}
- className="spacer-left nowrap note"
- >
- {lastAnalysisDate}
- </span>
- )}
- {displayVersion && (
- <span className="spacer-left nowrap note">{`${translate('version')} ${
- component.version
- }`}</span>
- )}
- {isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && (
- <HomePageSelect className="spacer-left" currentPage={currentPage} />
- )}
- </div>
- {isPullRequest(branchLike) && (
- <div className="navbar-context-meta-secondary display-inline-flex-center">
- {branchLike.url !== undefined && (
- <Link
- className="link-no-underline big-spacer-right"
- to={branchLike.url}
- target="_blank"
- size={12}
- >
- {translate('branches.see_the_pr')}
- </Link>
- )}
- <BranchStatus branchLike={branchLike} component={component} />
- </div>
+ <div className="sw-flex sw-items-center sw-flex-shrink sw-min-w-0">
+ <AnalysisStatus
+ component={component}
+ currentTask={currentTask}
+ currentTaskOnSameBranch={currentTaskOnSameBranch}
+ isInProgress={isInProgress}
+ isPending={isPending}
+ onWarningDismiss={props.onWarningDismiss}
+ warnings={warnings}
+ />
+ {branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />}
+ {component.version !== undefined && isABranch && (
+ <span className="sw-ml-4 sw-whitespace-nowrap">
+ {translateWithParameters('version_x', component.version)}
+ </span>
)}
- </>
+ {isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && (
+ <HomePageSelect className="sw-ml-4" currentPage={currentPage} />
+ )}
+ </div>
);
}
-export function getCurrentPage(component: Component, branchLike: BranchLike | undefined) {
- let currentPage: HomePage | undefined;
-
- const branch = isBranch(branchLike) && !branchLike.isMain ? branchLike.name : undefined;
-
- switch (component.qualifier) {
- case ComponentQualifier.Portfolio:
- case ComponentQualifier.SubPortfolio:
- currentPage = { type: 'PORTFOLIO', component: component.key };
- break;
- case ComponentQualifier.Application:
- currentPage = {
- type: 'APPLICATION',
- component: component.key,
- branch,
- };
- break;
- case ComponentQualifier.Project:
- // when home page is set to the default branch of a project, its name is returned as `undefined`
- currentPage = {
- type: 'PROJECT',
- component: component.key,
- branch,
- };
- break;
- }
-
- return currentPage;
-}
-
export default withCurrentUserContext(HeaderMeta);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { mockTask } from '../../../../../helpers/mocks/tasks';
+import { renderApp } from '../../../../../helpers/testReactTestingUtils';
+import { AnalysisErrorMessage } from '../AnalysisErrorMessage';
+
+it('should work when error is on a different branch', () => {
+ renderAnalysisErrorMessage({
+ currentTask: mockTask({ branch: 'branch-1.2' }),
+ currentTaskOnSameBranch: false,
+ });
+
+ 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', () => {
+ renderAnalysisErrorMessage({
+ currentTask: mockTask({ pullRequest: '2342', pullRequestTitle: 'Fix stuff' }),
+ currentTaskOnSameBranch: true,
+ });
+
+ expect(screen.getByText(/component_navigation.status.failed_X/)).toBeInTheDocument();
+ expect(screen.getByText(/2342 - 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 = '/'
+) {
+ return renderApp(
+ location,
+ <AnalysisErrorMessage
+ component={mockComponent()}
+ currentTask={mockTask()}
+ onLeave={jest.fn()}
+ currentTaskOnSameBranch={true}
+ {...overrides}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 }),
+ });
+}
it('renders correctly when there are warnings', () => {
renderComponentNav({ warnings: [mockTaskWarning()] });
expect(
- screen.getByText('component_navigation.last_analysis_had_warnings', { exact: false })
+ screen.getByText('project_navigation.analysis_status.warnings', { exact: false })
).toBeInTheDocument();
});
it('renders correctly when there is a background task in progress', () => {
renderComponentNav({ isInProgress: true });
expect(
- screen.getByText('component_navigation.status.in_progress', { exact: false })
+ 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('component_navigation.status.pending', { exact: false })
+ 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('component_navigation.status.failed_X', { exact: false })
+ screen.getByText('project_navigation.analysis_status.failed', { exact: false })
).toBeInTheDocument();
});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Alert } from '../../../../../components/ui/Alert';
-import { hasMessage } from '../../../../../helpers/l10n';
-import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTask } from '../../../../../helpers/mocks/tasks';
-import { mockLocation } from '../../../../../helpers/testMocks';
-import { Task, TaskStatuses, TaskTypes } from '../../../../../types/tasks';
-import { ComponentNavBgTaskNotif } from '../ComponentNavBgTaskNotif';
-
-jest.mock('../../../../../helpers/l10n', () => ({
- ...jest.requireActual('../../../../../helpers/l10n'),
- hasMessage: jest.fn().mockReturnValue(true),
-}));
-
-const UNKNOWN_TASK_TYPE: TaskTypes = 'UNKOWN' as TaskTypes;
-
-it('renders correctly', () => {
- expect(shallowRender()).toMatchSnapshot('default');
- expect(
- shallowRender({
- currentTask: mockTask({
- status: TaskStatuses.Failed,
- errorType: 'LICENSING',
- errorMessage: 'Foo',
- }),
- })
- ).toMatchSnapshot('license issue');
- expect(shallowRender({ currentTask: undefined }).type()).toBeNull(); // No task.
-});
-
-it.each([
- // failed
- [
- 'component_navigation.status.failed',
- 'error',
- mockTask({ status: TaskStatuses.Failed, type: UNKNOWN_TASK_TYPE }),
- false,
- false,
- false,
- false,
- ],
- [
- 'component_navigation.status.failed_X',
- 'error',
- mockTask({ status: TaskStatuses.Failed }),
- false,
- false,
- false,
- false,
- ],
- [
- 'component_navigation.status.failed.admin.link',
- 'error',
- mockTask({ status: TaskStatuses.Failed, type: UNKNOWN_TASK_TYPE }),
- false,
- false,
- true,
- false,
- ],
- [
- 'component_navigation.status.failed_X.admin.link',
- 'error',
- mockTask({ status: TaskStatuses.Failed }),
- false,
- false,
- true,
- false,
- ],
- [
- 'component_navigation.status.failed.admin.help',
- 'error',
- mockTask({ status: TaskStatuses.Failed, type: UNKNOWN_TASK_TYPE }),
- false,
- false,
- true,
- true,
- ],
- [
- 'component_navigation.status.failed_X.admin.help',
- 'error',
- mockTask({ status: TaskStatuses.Failed }),
- false,
- false,
- true,
- true,
- ],
- // failed_branch
- [
- 'component_navigation.status.failed_branch',
- 'error',
- mockTask({ status: TaskStatuses.Failed, branch: 'foo', type: UNKNOWN_TASK_TYPE }),
- false,
- false,
- false,
- false,
- ],
- [
- 'component_navigation.status.failed_branch_X',
- 'error',
- mockTask({ status: TaskStatuses.Failed, branch: 'foo' }),
- false,
- false,
- false,
- false,
- ],
- [
- 'component_navigation.status.failed_branch.admin.link',
- 'error',
- mockTask({ status: TaskStatuses.Failed, branch: 'foo', type: UNKNOWN_TASK_TYPE }),
- false,
- false,
- true,
- false,
- ],
- [
- 'component_navigation.status.failed_branch_X.admin.link',
- 'error',
- mockTask({ status: TaskStatuses.Failed, branch: 'foo' }),
- false,
- false,
- true,
- false,
- ],
- [
- 'component_navigation.status.failed_branch.admin.help',
- 'error',
- mockTask({ status: TaskStatuses.Failed, branch: 'foo', type: UNKNOWN_TASK_TYPE }),
- false,
- false,
- true,
- true,
- ],
- [
- 'component_navigation.status.failed_branch_X.admin.help',
- 'error',
- mockTask({ status: TaskStatuses.Failed, branch: 'foo' }),
- false,
- false,
- true,
- true,
- ],
- // pending
- [
- 'component_navigation.status.pending',
- 'info',
- mockTask({ type: UNKNOWN_TASK_TYPE }),
- true,
- false,
- false,
- false,
- ],
- ['component_navigation.status.pending_X', 'info', mockTask(), true, false, false, false],
- [
- 'component_navigation.status.pending.admin.link',
- 'info',
- mockTask({ type: UNKNOWN_TASK_TYPE }),
- true,
- false,
- true,
- false,
- ],
- [
- 'component_navigation.status.pending_X.admin.link',
- 'info',
- mockTask(),
- true,
- false,
- true,
- false,
- ],
- [
- 'component_navigation.status.pending.admin.help',
- 'info',
- mockTask({ type: UNKNOWN_TASK_TYPE }),
- true,
- false,
- true,
- true,
- ],
- [
- 'component_navigation.status.pending_X.admin.help',
- 'info',
- mockTask({ status: TaskStatuses.Failed }),
- true,
- false,
- true,
- true,
- ],
- // in_progress
- [
- 'component_navigation.status.in_progress',
- 'info',
- mockTask({ type: UNKNOWN_TASK_TYPE }),
- true,
- true,
- false,
- false,
- ],
- ['component_navigation.status.in_progress_X', 'info', mockTask(), true, true, false, false],
- [
- 'component_navigation.status.in_progress.admin.link',
- 'info',
- mockTask({ type: UNKNOWN_TASK_TYPE }),
- true,
- true,
- true,
- false,
- ],
- [
- 'component_navigation.status.in_progress_X.admin.link',
- 'info',
- mockTask(),
- true,
- true,
- true,
- false,
- ],
- [
- 'component_navigation.status.in_progress.admin.help',
- 'info',
- mockTask({ type: UNKNOWN_TASK_TYPE }),
- true,
- true,
- true,
- true,
- ],
- [
- 'component_navigation.status.in_progress_X.admin.help',
- 'info',
- mockTask({ status: TaskStatuses.Failed }),
- true,
- true,
- true,
- true,
- ],
-])(
- 'should render the expected message=%p',
- (
- expectedMessage: string,
- alertVariant: string,
- currentTask: Task,
- isPending: boolean,
- isInProgress: boolean,
- showBackgroundTasks: boolean,
- onBackgroudTaskPage: boolean
- ) => {
- if (currentTask.type === UNKNOWN_TASK_TYPE) {
- (hasMessage as jest.Mock).mockReturnValueOnce(false);
- }
-
- const wrapper = shallowRender({
- component: mockComponent({ configuration: { showBackgroundTasks } }),
- currentTask,
- currentTaskOnSameBranch: !currentTask.branch,
- isPending,
- isInProgress,
- location: mockLocation({
- pathname: onBackgroudTaskPage ? '/project/background_tasks' : '/foo/bar',
- }),
- });
- const messageProps = wrapper.find(FormattedMessage).props();
-
- // Translation key.
- expect(messageProps.defaultMessage).toBe(expectedMessage);
-
- // Alert variant.
- expect(wrapper.find(Alert).props().variant).toBe(alertVariant);
-
- // Formatted message values prop.
- // eslint-disable-next-line jest/no-conditional-in-test
- if (/_X/.test(expectedMessage)) {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.type).toBe(`background_task.type.${currentTask.type}`);
- } else {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.type).toBeUndefined();
- }
-
- // eslint-disable-next-line jest/no-conditional-in-test
- if (currentTask.branch) {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.branch).toBe(currentTask.branch);
- } else {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.branch).toBeUndefined();
- }
-
- // eslint-disable-next-line jest/no-conditional-in-test
- if (showBackgroundTasks) {
- // eslint-disable-next-line jest/no-conditional-in-test
- if (onBackgroudTaskPage) {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.url).toBeUndefined();
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.stacktrace).toBe('background_tasks.show_stacktrace');
- } else {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.url).toBeDefined();
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.stacktrace).toBeUndefined();
- }
- } else {
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.url).toBeUndefined();
- // eslint-disable-next-line jest/no-conditional-expect
- expect(messageProps.values?.stacktrace).toBeUndefined();
- }
- }
-);
-
-function shallowRender(props: Partial<ComponentNavBgTaskNotif['props']> = {}) {
- return shallow<ComponentNavBgTaskNotif>(
- <ComponentNavBgTaskNotif
- component={mockComponent()}
- currentTask={mockTask({ status: TaskStatuses.Failed })}
- location={mockLocation()}
- {...props}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { isValidLicense } from '../../../../../api/editions';
-import { mockTask } from '../../../../../helpers/mocks/tasks';
-import { mockAppState } from '../../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../../helpers/testUtils';
-import { TaskStatuses } from '../../../../../types/tasks';
-import { ComponentNavLicenseNotif } from '../ComponentNavLicenseNotif';
-
-jest.mock('../../../../../helpers/l10n', () => ({
- ...jest.requireActual('../../../../../helpers/l10n'),
- hasMessage: jest.fn().mockReturnValue(true),
-}));
-
-jest.mock('../../../../../api/editions', () => ({
- isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: false }),
-}));
-
-beforeEach(() => {
- (isValidLicense as jest.Mock<any>).mockClear();
-});
-
-it('renders background task license info correctly', async () => {
- let wrapper = getWrapper({
- currentTask: mockTask({
- status: TaskStatuses.Failed,
- errorType: 'LICENSING',
- errorMessage: 'Foo',
- }),
- });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-
- wrapper = getWrapper({
- appState: mockAppState({ canAdmin: false }),
- currentTask: mockTask({
- status: TaskStatuses.Failed,
- errorType: 'LICENSING',
- errorMessage: 'Foo',
- }),
- });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders a different message if the license is valid', async () => {
- (isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true });
- const wrapper = getWrapper({
- currentTask: mockTask({
- status: TaskStatuses.Failed,
- errorType: 'LICENSING',
- errorMessage: 'Foo',
- }),
- });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders correctly for LICENSING_LOC error', async () => {
- (isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true });
- const wrapper = getWrapper({
- currentTask: mockTask({
- status: TaskStatuses.Failed,
- errorType: 'LICENSING_LOC',
- errorMessage: 'Foo',
- }),
- });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-function getWrapper(props: Partial<ComponentNavLicenseNotif['props']> = {}) {
- return shallow(
- <ComponentNavLicenseNotif
- appState={mockAppState({ canAdmin: true })}
- currentTask={mockTask({ errorMessage: 'Foo', errorType: 'LICENSING' })}
- {...props}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockTaskWarning } from '../../../../../helpers/mocks/tasks';
-import ComponentNavWarnings from '../ComponentNavWarnings';
-
-it('should render', () => {
- const wrapper = shallow(
- <ComponentNavWarnings
- componentKey="foo"
- isBranch={true}
- onWarningDismiss={jest.fn()}
- warnings={[mockTaskWarning({ message: 'warning 1' })]}
- />
- );
- wrapper.setState({ modal: true });
- expect(wrapper).toMatchSnapshot();
-});
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import HomePageSelect from '../../../../../components/controls/HomePageSelect';
import { mockBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTaskWarning } from '../../../../../helpers/mocks/tasks';
-import { mockCurrentUser } from '../../../../../helpers/testMocks';
-import { ComponentQualifier } from '../../../../../types/component';
-import { getCurrentPage, HeaderMeta, HeaderMetaProps } from '../HeaderMeta';
-
-jest.mock('react-intl', () => ({
- useIntl: jest.fn().mockImplementation(() => ({
- formatDate: jest.fn().mockImplementation(() => '2017-01-02T00:00:00.000Z'),
- })),
-}));
-
-it('should render correctly for a branch', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
+import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks';
+import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
+import { renderApp } from '../../../../../helpers/testReactTestingUtils';
+import { TaskStatuses } from '../../../../../types/tasks';
+import { CurrentUser } from '../../../../../types/users';
+import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta';
+
+it('should render correctly for a branch with warnings', async () => {
+ const user = userEvent.setup();
+
+ renderHeaderMeta();
+
+ expect(screen.getByText('version_x.0.0.1')).toBeInTheDocument();
+
+ expect(screen.getByText('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 render correctly for a main project branch', () => {
- const wrapper = shallowRender({
- branchLike: mockBranch({ isMain: true }),
- });
- expect(wrapper).toMatchSnapshot();
+it('should handle a branch with missing version and no warnings', () => {
+ renderHeaderMeta({ component: mockComponent({ version: undefined }), warnings: [] });
+
+ expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
+ expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument();
});
-it('should render correctly for a portfolio', () => {
- const wrapper = shallowRender({
- component: mockComponent({ key: 'foo', qualifier: ComponentQualifier.Portfolio }),
+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(wrapper).toMatchSnapshot();
+
+ expect(screen.getByText('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', () => {
- const wrapper = shallowRender({
+ renderHeaderMeta({
branchLike: mockPullRequest({
url: 'https://example.com/pull/1234',
}),
});
- expect(wrapper).toMatchSnapshot();
-});
-it('should render correctly when the user is not logged in', () => {
- const wrapper = shallowRender({ currentUser: { isLoggedIn: false, dismissedNotices: {} } });
- expect(wrapper.find(HomePageSelect).exists()).toBe(false);
+ expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
+ expect(screen.getByText('branch_like_navigation.for_merge_into_x_from_y')).toBeInTheDocument();
});
-describe('#getCurrentPage', () => {
- it('should return a portfolio page', () => {
- expect(
- getCurrentPage(
- mockComponent({ key: 'foo', qualifier: ComponentQualifier.Portfolio }),
- undefined
- )
- ).toEqual({
- type: 'PORTFOLIO',
- component: 'foo',
- });
- });
-
- it('should return an application page', () => {
- expect(
- getCurrentPage(
- mockComponent({ key: 'foo', qualifier: ComponentQualifier.Application }),
- mockBranch({ name: 'develop' })
- )
- ).toEqual({ type: 'APPLICATION', component: 'foo', branch: 'develop' });
- });
-
- it('should return a project page', () => {
- expect(getCurrentPage(mockComponent(), mockBranch({ name: 'feature/foo' }))).toEqual({
- type: 'PROJECT',
- component: 'my-project',
- branch: 'feature/foo',
- });
- });
+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 shallowRender(props: Partial<HeaderMetaProps> = {}) {
- return shallow(
+function renderHeaderMeta(
+ props: Partial<HeaderMetaProps> = {},
+ currentUser: CurrentUser = mockLoggedInUser()
+) {
+ return renderApp(
+ '/',
<HeaderMeta
branchLike={mockBranch()}
- component={mockComponent({ analysisDate: '2017-01-02T00:00:00.000Z', version: '0.0.1' })}
- currentUser={mockCurrentUser({ isLoggedIn: true })}
+ component={mockComponent({ version: '0.0.1' })}
onWarningDismiss={jest.fn()}
- warnings={[mockTaskWarning({ message: 'ERROR_1' }), mockTaskWarning({ message: 'ERROR_2' })]}
+ warnings={[
+ mockTaskWarning({ key: '1', message: 'ERROR_1' }),
+ mockTaskWarning({ key: '2', message: 'ERROR_2' }),
+ ]}
{...props}
- />
+ />,
+ { currentUser }
);
}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly: default 1`] = `
-<Alert
- className="null-spacer-bottom"
- display="banner"
- variant="error"
->
- <FormattedMessage
- defaultMessage="component_navigation.status.failed_X"
- id="component_navigation.status.failed_X"
- values={
- {
- "branch": undefined,
- "stacktrace": undefined,
- "type": "background_task.type.REPORT",
- "url": undefined,
- }
- }
- />
-</Alert>
-`;
-
-exports[`renders correctly: license issue 1`] = `
-<withAppStateContext(ComponentNavLicenseNotif)
- currentTask={
- {
- "analysisId": "x123",
- "componentKey": "foo",
- "componentName": "Foo",
- "componentQualifier": "TRK",
- "errorMessage": "Foo",
- "errorType": "LICENSING",
- "id": "AXR8jg_0mF2ZsYr8Wzs2",
- "status": "FAILED",
- "submittedAt": "2020-09-11T11:45:35+0200",
- "type": "REPORT",
- }
- }
-/>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders a different message if the license is valid 1`] = `
-<Alert
- display="banner"
- variant="error"
->
- component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK
-</Alert>
-`;
-
-exports[`renders background task license info correctly 1`] = `
-<Alert
- display="banner"
- variant="error"
->
- <span
- className="little-spacer-right"
- >
- Foo
- </span>
- <ForwardRef(Link)
- to="/admin/extension/license/app"
- >
- license.component_navigation.button.LICENSING
- .
- </ForwardRef(Link)>
-</Alert>
-`;
-
-exports[`renders background task license info correctly 2`] = `
-<Alert
- display="banner"
- variant="error"
->
- <span
- className="little-spacer-right"
- >
- Foo
- </span>
- please_contact_administrator
-</Alert>
-`;
-
-exports[`renders correctly for LICENSING_LOC error 1`] = `
-<Alert
- display="banner"
- variant="error"
->
- <span
- className="little-spacer-right"
- >
- Foo
- </span>
- <ForwardRef(Link)
- to="/admin/extension/license/app"
- >
- license.component_navigation.button.LICENSING_LOC
- .
- </ForwardRef(Link)>
-</Alert>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<Fragment>
- <Alert
- className="js-component-analysis-warnings flex-1"
- display="inline"
- variant="warning"
- >
- <FormattedMessage
- defaultMessage="component_navigation.last_analysis_had_warnings"
- id="component_navigation.last_analysis_had_warnings"
- values={
- {
- "branchType": "branches.branch",
- "warnings": <a
- href="#"
- onClick={[Function]}
- >
- <FormattedMessage
- defaultMessage="component_navigation.x_warnings"
- id="component_navigation.x_warnings"
- values={
- {
- "warningsCount": 1,
- }
- }
- />
- </a>,
- }
- }
- />
- </Alert>
- <withCurrentUserContext(AnalysisWarningsModal)
- componentKey="foo"
- onClose={[Function]}
- onWarningDismiss={[MockFunction]}
- warnings={
- [
- {
- "dismissable": false,
- "key": "foo",
- "message": "warning 1",
- },
- ]
- }
- />
-</Fragment>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly for a branch 1`] = `
-<Fragment>
- <div
- className="display-flex-center flex-0 small"
- >
- <span
- className="header-meta-warnings"
- >
- <ComponentNavWarnings
- componentKey="my-project"
- isBranch={true}
- onWarningDismiss={[MockFunction]}
- warnings={
- [
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_1",
- },
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_2",
- },
- ]
- }
- />
- </span>
- <span
- className="spacer-left nowrap note"
- title="overview.project.last_analysis.date_time.2017-01-02T00:00:00.000Z"
- >
- 2017-01-02T00:00:00.000Z
- </span>
- <span
- className="spacer-left nowrap note"
- >
- version 0.0.1
- </span>
- <withCurrentUserContext(HomePageSelect)
- className="spacer-left"
- currentPage={
- {
- "branch": "branch-6.7",
- "component": "my-project",
- "type": "PROJECT",
- }
- }
- />
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly for a main project branch 1`] = `
-<Fragment>
- <div
- className="display-flex-center flex-0 small"
- >
- <span
- className="header-meta-warnings"
- >
- <ComponentNavWarnings
- componentKey="my-project"
- isBranch={true}
- onWarningDismiss={[MockFunction]}
- warnings={
- [
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_1",
- },
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_2",
- },
- ]
- }
- />
- </span>
- <span
- className="spacer-left nowrap note"
- title="overview.project.last_analysis.date_time.2017-01-02T00:00:00.000Z"
- >
- 2017-01-02T00:00:00.000Z
- </span>
- <span
- className="spacer-left nowrap note"
- >
- version 0.0.1
- </span>
- <withCurrentUserContext(HomePageSelect)
- className="spacer-left"
- currentPage={
- {
- "branch": undefined,
- "component": "my-project",
- "type": "PROJECT",
- }
- }
- />
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly for a portfolio 1`] = `
-<Fragment>
- <div
- className="display-flex-center flex-0 small"
- >
- <span
- className="header-meta-warnings"
- >
- <ComponentNavWarnings
- componentKey="foo"
- isBranch={true}
- onWarningDismiss={[MockFunction]}
- warnings={
- [
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_1",
- },
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_2",
- },
- ]
- }
- />
- </span>
- <withCurrentUserContext(HomePageSelect)
- className="spacer-left"
- currentPage={
- {
- "component": "foo",
- "type": "PORTFOLIO",
- }
- }
- />
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly for a pull request 1`] = `
-<Fragment>
- <div
- className="display-flex-center flex-0 small"
- >
- <span
- className="header-meta-warnings"
- >
- <ComponentNavWarnings
- componentKey="my-project"
- isBranch={false}
- onWarningDismiss={[MockFunction]}
- warnings={
- [
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_1",
- },
- {
- "dismissable": false,
- "key": "foo",
- "message": "ERROR_2",
- },
- ]
- }
- />
- </span>
- <span
- className="spacer-left nowrap note"
- title="overview.project.last_analysis.date_time.2017-01-02T00:00:00.000Z"
- >
- 2017-01-02T00:00:00.000Z
- </span>
- </div>
- <div
- className="navbar-context-meta-secondary display-inline-flex-center"
- >
- <ForwardRef(Link)
- className="link-no-underline big-spacer-right"
- size={12}
- target="_blank"
- to="https://example.com/pull/1234"
- >
- branches.see_the_pr
- </ForwardRef(Link)>
- <withBranchStatus(BranchStatus)
- branchLike={
- {
- "analysisDate": "2018-01-01",
- "base": "master",
- "branch": "feature/foo/bar",
- "key": "1001",
- "target": "master",
- "title": "Foo Bar feature",
- "url": "https://example.com/pull/1234",
- }
- }
- component={
- {
- "analysisDate": "2017-01-02T00:00:00.000Z",
- "breadcrumbs": [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "TRK",
- "qualityGate": {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": [
- {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": [],
- "version": "0.0.1",
- }
- }
- />
- </div>
-</Fragment>
-`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { mockBranch } from '../../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../../helpers/mocks/component';
+import { ComponentQualifier } from '../../../../../types/component';
+import { getCurrentPage } from '../utils';
+
+describe('getCurrentPage', () => {
+ it('should return a portfolio page', () => {
+ expect(
+ getCurrentPage(
+ mockComponent({ key: 'foo', qualifier: ComponentQualifier.Portfolio }),
+ undefined
+ )
+ ).toEqual({
+ type: 'PORTFOLIO',
+ component: 'foo',
+ });
+ });
+
+ it('should return a portfolio page for a subportfolio too', () => {
+ expect(
+ getCurrentPage(
+ mockComponent({ key: 'foo', qualifier: ComponentQualifier.SubPortfolio }),
+ undefined
+ )
+ ).toEqual({
+ type: 'PORTFOLIO',
+ component: 'foo',
+ });
+ });
+
+ it('should return an application page', () => {
+ expect(
+ getCurrentPage(
+ mockComponent({ key: 'foo', qualifier: ComponentQualifier.Application }),
+ mockBranch({ name: 'develop' })
+ )
+ ).toEqual({ type: 'APPLICATION', component: 'foo', branch: 'develop' });
+ });
+
+ it('should return a project page', () => {
+ expect(getCurrentPage(mockComponent(), mockBranch({ name: 'feature/foo' }))).toEqual({
+ type: 'PROJECT',
+ component: 'my-project',
+ branch: 'feature/foo',
+ });
+ });
+});
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { isPullRequest } from '../../../../../helpers/branch-like';
-import { translate } from '../../../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../../../helpers/l10n';
import { BranchLike } from '../../../../../types/branch-like';
export interface CurrentBranchLikeMergeInformationProps {
}
return (
- <span className="big-spacer-left flex-shrink note text-ellipsis">
+ <span
+ className="sw-overflow-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-mx-1 sw-flex-shrink sw-min-w-0"
+ title={translateWithParameters(
+ 'branch_like_navigation.for_merge_into_x_from_y.title',
+ currentBranchLike.target,
+ currentBranchLike.branch
+ )}
+ >
<FormattedMessage
defaultMessage={translate('branch_like_navigation.for_merge_into_x_from_y')}
id="branch_like_navigation.for_merge_into_x_from_y"
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockMainBranch, mockPullRequest } from '../../../../../../helpers/mocks/branch-like';
-import {
- CurrentBranchLikeMergeInformation,
- CurrentBranchLikeMergeInformationProps,
-} from '../CurrentBranchLikeMergeInformation';
-
-it('should render correctly', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should not render for non-pull-request branch like', () => {
- const wrapper = shallowRender({ currentBranchLike: mockMainBranch() });
- expect(wrapper.type()).toBeNull();
-});
-
-function shallowRender(props?: Partial<CurrentBranchLikeMergeInformationProps>) {
- return shallow(
- <CurrentBranchLikeMergeInformation currentBranchLike={mockPullRequest()} {...props} />
- );
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<span
- className="big-spacer-left flex-shrink note text-ellipsis"
->
- <FormattedMessage
- defaultMessage="branch_like_navigation.for_merge_into_x_from_y"
- id="branch_like_navigation.for_merge_into_x_from_y"
- values={
- {
- "branch": <strong>
- feature/foo/bar
- </strong>,
- "target": <strong>
- master
- </strong>,
- }
- }
- />
-</span>
-`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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-2023 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 { isBranch } from '../../../../helpers/branch-like';
+import { BranchLike } from '../../../../types/branch-like';
+import { ComponentQualifier } from '../../../../types/component';
+import { Component } from '../../../../types/types';
+import { HomePage } from '../../../../types/users';
+
+export function getCurrentPage(component: Component, branchLike: BranchLike | undefined) {
+ let currentPage: HomePage | undefined;
+
+ const branch = isBranch(branchLike) && !branchLike.isMain ? branchLike.name : undefined;
+
+ switch (component.qualifier) {
+ case ComponentQualifier.Portfolio:
+ case ComponentQualifier.SubPortfolio:
+ currentPage = { type: 'PORTFOLIO', component: component.key };
+ break;
+ case ComponentQualifier.Application:
+ currentPage = {
+ type: 'APPLICATION',
+ component: component.key,
+ branch,
+ };
+ break;
+ case ComponentQualifier.Project:
+ // when home page is set to the default branch of a project, its name is returned as `undefined`
+ currentPage = {
+ type: 'PROJECT',
+ component: component.key,
+ branch,
+ };
+ break;
+ }
+
+ return currentPage;
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { BareButton, HomeFillIcon, HomeIcon, Tooltip } from 'design-system';
import * as React from 'react';
import { setHomePage } from '../../api/users';
import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import { ButtonLink } from '../../components/controls/buttons';
-import Tooltip from '../../components/controls/Tooltip';
-import HomeIcon from '../../components/icons/HomeIcon';
import { translate } from '../../helpers/l10n';
import { isSameHomePage } from '../../helpers/users';
import { HomePage, isLoggedIn } from '../../types/users';
export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' };
export class HomePageSelect extends React.PureComponent<Props> {
- buttonNode?: HTMLElement | null;
-
async setCurrentUserHomepage(homepage: HomePage) {
const { currentUser } = this.props;
- if (currentUser && isLoggedIn(currentUser)) {
+ if (isLoggedIn(currentUser)) {
await setHomePage(homepage);
this.props.updateCurrentUserHomepage(homepage);
-
- if (this.buttonNode) {
- this.buttonNode.focus();
- }
}
}
className={classNames('display-inline-block', className)}
role="img"
>
- <HomeIcon filled={isChecked} />
+ <HomeFillIcon />
</span>
) : (
- <ButtonLink
+ <BareButton
aria-label={tooltip}
- className={classNames('link-no-underline', 'set-homepage-link', className)}
+ className={className}
onClick={isChecked ? this.handleReset : this.handleClick}
- innerRef={(node) => (this.buttonNode = node)}
>
- <HomeIcon filled={isChecked} />
- </ButtonLink>
+ {isChecked ? <HomeFillIcon /> : <HomeIcon />}
+ </BareButton>
)}
</Tooltip>
);
* 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 { setHomePage } from '../../../api/users';
import { mockLoggedInUser } from '../../../helpers/testMocks';
}));
it('renders and behaves correctly', async () => {
+ const user = userEvent.setup();
const updateCurrentUserHomepage = jest.fn();
renderHomePageSelect({ updateCurrentUserHomepage });
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
- button.click();
+ await user.click(button);
await new Promise(setImmediate);
expect(setHomePage).toHaveBeenCalledWith({ type: 'MY_PROJECTS' });
expect(updateCurrentUserHomepage).toHaveBeenCalled();
});
it('renders correctly if user is on the homepage', async () => {
+ const user = userEvent.setup();
+
renderHomePageSelect({ currentUser: mockLoggedInUser({ homepage: { type: 'MY_PROJECTS' } }) });
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
- button.click();
+ await user.click(button);
await new Promise(setImmediate);
expect(setHomePage).toHaveBeenCalledWith(DEFAULT_HOMEPAGE);
expect(button).toHaveFocus();
value=Value
variation=Variation
version=Version
+version_x=Version {0}
view=View
views=Views
violations=Violations
custom_measures.metric=Metric
+#------------------------------------------------------------------------------
+#
+# PROJECT NAVIGATION
+#
+#------------------------------------------------------------------------------
+
+project_navigation.analysis_status.failed=The last analysis has failed.
+project_navigation.analysis_status.warnings=The last analysis has warnings.
+project_navigation.analysis_status.pending=New analysis pending
+project_navigation.analysis_status.in_progress=New analysis in progress
+project_navigation.analysis_status.details_link=See details
+
#------------------------------------------------------------------------------
#
# PROJECT ACTIVITY/HISTORY SERVICE
plugin_risk_consent.description2=Plugins are not provided by SonarSource and are therefore installed at your own risk. SonarSource disclaims all liability for installing and using such plugins.
plugin_risk_consent.action=I understand the risk
+
#------------------------------------------------------------------------------
#
# BACKGROUND TASKS
component_navigation.status.in_progress_X.admin.help=The {type} is in progress.
component_navigation.status.last_blocked_due_to_bad_license_X=Last analysis blocked due to an invalid license, which has since been corrected. Please reanalyze this {0}.
-component_navigation.last_analysis_had_warnings=Last analysis of this {branchType} had {warnings}
-component_navigation.x_warnings={warningsCount} {warningsCount, plural, one {warning} other {warnings}}
-
component_navigation.pr_deco.error_detected_X=We've detected an issue with your configuration. Your SonarQube instance won't be able to perform any pull request decoration. {action}
component_navigation.pr_deco.action.check_project_settings=Please check your project settings.
component_navigation.pr_deco.action.contact_project_admin=Please contact your project administrator.
branch_like_navigation.orphan_pull_requests=Orphan Pull Requests
branch_like_navigation.orphan_pull_requests.tooltip=When the base of a Pull Request is deleted, this Pull Request becomes orphan.
branch_like_navigation.for_merge_into_x_from_y=for merge into {target} from {branch}
+branch_like_navigation.for_merge_into_x_from_y.title=for merge into {0} from {1}
branch_like_navigation.no_branch_support.title=Get the most out of SonarQube with branch and PR/MR analysis
branch_like_navigation.no_branch_support.title.pr=Get the most out of SonarQube with branch and PR analysis
branch_like_navigation.no_branch_support.title.mr=Get the most out of SonarQube with branch and MR analysis