]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18776 New UI for Header Meta (analysis status, homepage, version)
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 16 Mar 2023 12:53:23 +0000 (13:53 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 27 Mar 2023 20:03:03 +0000 (20:03 +0000)
41 files changed:
server/sonar-web/design-system/src/components/FlagMessage.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/FlagErrorIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/FlagInfoIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/FlagSuccessIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/FlagWarningIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/HomeFillIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/HomeIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/scripts/build-design-system.js
server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.css [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBgTaskNotif-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/utils-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/src/components/FlagMessage.tsx b/server/sonar-web/design-system/src/components/FlagMessage.tsx
new file mode 100644 (file)
index 0000000..3a3aed0
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * 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')};
+`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx b/server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx
new file mode 100644 (file)
index 0000000..b51dddc
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/icons/FlagErrorIcon.tsx b/server/sonar-web/design-system/src/components/icons/FlagErrorIcon.tsx
new file mode 100644 (file)
index 0000000..519b9a3
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/icons/FlagInfoIcon.tsx b/server/sonar-web/design-system/src/components/icons/FlagInfoIcon.tsx
new file mode 100644 (file)
index 0000000..3aef304
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/icons/FlagSuccessIcon.tsx b/server/sonar-web/design-system/src/components/icons/FlagSuccessIcon.tsx
new file mode 100644 (file)
index 0000000..748b8a5
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/icons/FlagWarningIcon.tsx b/server/sonar-web/design-system/src/components/icons/FlagWarningIcon.tsx
new file mode 100644 (file)
index 0000000..0550bbb
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/icons/HomeFillIcon.tsx b/server/sonar-web/design-system/src/components/icons/HomeFillIcon.tsx
new file mode 100644 (file)
index 0000000..72d7cd6
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/icons/HomeIcon.tsx b/server/sonar-web/design-system/src/components/icons/HomeIcon.tsx
new file mode 100644 (file)
index 0000000..a4a6d07
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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);
index 8b30b791711f35de073d6dcdf267f47a600d1830..3b681fbe1b8e61b93a1dc457ef94a4288d2c82db 100644 (file)
  * 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';
index e7bdcf4ca80302a6e4c9da76a4b5361a85073f21..452c570433e26845ab8b36e45378092345c37e8b 100644 (file)
@@ -24,6 +24,7 @@ export { default as DeferredSpinner } from './DeferredSpinner';
 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';
index 737357a2c37453f9a723248435864b79f1e1e77f..7c4241f1a293872e349659eafe8b13f7c7af99bc 100644 (file)
@@ -36,8 +36,8 @@ function buildDesignSystem(callback) {
     console.log(chalk.red.bold(data.toString()));
   });
 
-  build.on('exit', (code) => {
-    if (code === 0) {
+  build.on('exit', function (code) {
+    if (code === 0 && callback) {
       callback();
     }
   });
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx
new file mode 100644 (file)
index 0000000..00b8bda
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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 }}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx
new file mode 100644 (file)
index 0000000..46dc0f7
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx
new file mode 100644 (file)
index 0000000..0c4e6f6
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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')
+      )}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx
new file mode 100644 (file)
index 0000000..c08700a
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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;
+}
index 1c65589ead7a7d660285755146f377f27dfe54a7..8ca22c7edfbccae708270d303a6b0074bce4fd75 100644 (file)
@@ -27,11 +27,10 @@ import {
 } 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';
@@ -91,20 +90,6 @@ export default function ComponentNav(props: ComponentNavProps) {
 
   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} />;
@@ -120,12 +105,7 @@ export default function ComponentNav(props: ComponentNavProps) {
       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', {
@@ -142,6 +122,10 @@ export default function ComponentNav(props: ComponentNavProps) {
         <HeaderMeta
           branchLike={currentBranchLike}
           component={component}
+          currentTask={currentTask}
+          currentTaskOnSameBranch={currentTaskOnSameBranch}
+          isInProgress={isInProgress}
+          isPending={isPending}
           onWarningDismiss={props.onWarningDismiss}
           warnings={warnings}
         />
@@ -152,7 +136,9 @@ export default function ComponentNav(props: ComponentNavProps) {
         component={component}
         isInProgress={isInProgress}
         isPending={isPending}
-        onToggleProjectInfo={() => setDisplayProjectInfo(!displayProjectInfo)}
+        onToggleProjectInfo={() => {
+          setDisplayProjectInfo(!displayProjectInfo);
+        }}
         projectInfoDisplayed={displayProjectInfo}
       />
       <InfoDrawer
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx
deleted file mode 100644 (file)
index 50070da..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx
deleted file mode 100644 (file)
index 54be331..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
deleted file mode 100644 (file)
index 1ef62b3..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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}
-          />
-        )}
-      </>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.css b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.css
deleted file mode 100644 (file)
index 93e8cf7..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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;
-}
index 4362e6c0191288d0e687c42b47dffd7e231261dd..1cff50e993b1f09d40eae46aa109b374eddcbc26 100644 (file)
  * 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);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx
new file mode 100644 (file)
index 0000000..e3be72d
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx
new file mode 100644 (file)
index 0000000..5405df2
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 }),
+  });
+}
index e7235f579e61c21e68c0255474adedb3424ebadb..81b2ea7fc3b24ad3ba739651fc1671922f58419b 100644 (file)
@@ -30,28 +30,28 @@ import ComponentNav, { ComponentNavProps } from '../ComponentNav';
 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();
 });
 
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBgTaskNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBgTaskNotif-test.tsx
deleted file mode 100644 (file)
index b9386f3..0000000
+++ /dev/null
@@ -1,341 +0,0 @@
-/*
- * 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}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx
deleted file mode 100644 (file)
index 103ca94..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx
deleted file mode 100644 (file)
index c19a6d6..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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();
-});
index 1dcf15bb587cd752afcd439e2920688bcca42cb8..cb92dda3fa194a193dbb122cbdda89a7354fddfa 100644 (file)
  * 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 }
   );
 }
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap
deleted file mode 100644 (file)
index 1b40f53..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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",
-    }
-  }
-/>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap
deleted file mode 100644 (file)
index 415d696..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
deleted file mode 100644 (file)
index a09a740..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-// 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>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap
deleted file mode 100644 (file)
index 2977276..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-// 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>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/utils-test.ts b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/utils-test.ts
new file mode 100644 (file)
index 0000000..c2c7896
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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',
+    });
+  });
+});
index e78d5686e1146517de4b0f17affe56d684ead664..393483e0fe1a707d66c9286cef7ad7f0ce888dd5 100644 (file)
@@ -20,7 +20,7 @@
 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 {
@@ -35,7 +35,14 @@ export function CurrentBranchLikeMergeInformation(props: CurrentBranchLikeMergeI
   }
 
   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"
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx
deleted file mode 100644 (file)
index 32f1bbe..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap
deleted file mode 100644 (file)
index 5e4d7cd..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts b/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts
new file mode 100644 (file)
index 0000000..7353210
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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];
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/utils.ts b/server/sonar-web/src/main/js/app/components/nav/component/utils.ts
new file mode 100644 (file)
index 0000000..f8e36b7
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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;
+}
index fec78c2f8356e4c7d8016978380f7df734ec777b..cb65d92527a1d421a06d0632ed8c611fba3f921e 100644 (file)
  * 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';
@@ -38,19 +36,13 @@ interface Props
 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();
-      }
     }
   }
 
@@ -84,17 +76,16 @@ export class HomePageSelect extends React.PureComponent<Props> {
             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>
     );
index 3cefed0190ae7e18e5bd8d793d76b0544f7c7a2a..77128773d2070659e1d55342c37709d1ac5769b0 100644 (file)
@@ -18,6 +18,7 @@
  * 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';
@@ -29,12 +30,13 @@ jest.mock('../../../api/users', () => ({
 }));
 
 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();
@@ -42,11 +44,13 @@ it('renders and behaves correctly', async () => {
 });
 
 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();
index d339498d9391a25817e99acf96ea7e63d3c5f63c..7d1f9f70f6a321a15a51ae58033b4c5ccd7dc9bc 100644 (file)
@@ -231,6 +231,7 @@ user=User
 value=Value
 variation=Variation
 version=Version
+version_x=Version {0}
 view=View
 views=Views
 violations=Violations
@@ -1550,6 +1551,18 @@ custom_measures.update_custom_measure=Update Custom Measure
 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
@@ -3130,6 +3143,7 @@ plugin_risk_consent.description=A plugin has been detected.
 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
@@ -3161,9 +3175,6 @@ component_navigation.status.in_progress.admin.help=A background task is in progr
 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.
@@ -4261,6 +4272,7 @@ branch_like_navigation.pull_requests=Pull Requests
 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