]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21298 Showcase Echoes' Link and LinkStandalone components in a few places
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Wed, 28 Feb 2024 17:04:28 +0000 (18:04 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 28 Feb 2024 20:02:44 +0000 (20:02 +0000)
17 files changed:
server/sonar-web/design-system/src/components/Link.tsx
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx
server/sonar-web/src/main/js/app/components/KeyboardShortcutsModal.tsx
server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/maintenance/components/App.tsx
server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx
server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx
server/sonar-web/src/main/js/components/common/Link.tsx

index 2d50f249060d4201a7097d0b46fe976fc7b14216..5138a25c6434b2b96ff0a06cd308a3c2aa6a3c72 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import { css } from '@emotion/react';
 import styled from '@emotion/styled';
 import React, { HTMLAttributeAnchorTarget } from 'react';
@@ -26,6 +27,17 @@ import { themeBorder, themeColor } from '../helpers/theme';
 import { TooltipWrapperInner } from './Tooltip';
 import { OpenNewTabIcon } from './icons/OpenNewTabIcon';
 
+/** @deprecated Use LinkProps from Echoes instead.
+ *
+ * Some of the props have changed or been renamed:
+ * - `blurAfterClick` is now `shouldBlurAfterClick`
+ * - ~`disabled`~ doesn't exist anymore, a disabled link is just a regular text
+ * - `forceExternal` is now `isExternal`
+ * - `icon` is now `iconLeft` and can only be used with LinkStandalone
+ * - `preventDefault` is now `shouldPreventDefault`
+ * - `showExternalIcon` is now `hasExternalIcon`
+ * - `stopPropagation` is now `shouldStopPropagation`
+ */
 export interface LinkProps extends RouterLinkProps {
   blurAfterClick?: boolean;
   disabled?: boolean;
@@ -109,6 +121,8 @@ const ExternalIcon = styled(OpenNewTabIcon)`
   color: ${themeColor('linkExternalIcon')};
 `;
 
+/** @deprecated Use either Link or LinkStandalone from Echoes, or react-router-dom's Link instead.
+ */
 export const BaseLink = React.forwardRef(BaseLinkWithRef);
 
 const StyledBaseLink = styled(BaseLink)`
@@ -150,6 +164,8 @@ const StyledBaseLink = styled(BaseLink)`
     `};
 `;
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const NakedLink = styled(BaseLink)`
   border-bottom: none;
   padding-bottom: 1px;
@@ -167,6 +183,8 @@ export const NakedLink = styled(BaseLink)`
          }`};
 `;
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const DrilldownLink = styled(StyledBaseLink)`
   ${tw`sw-heading-lg`}
   ${tw`sw-tracking-tight`}
@@ -184,6 +202,8 @@ export const DrilldownLink = styled(StyledBaseLink)`
 
 DrilldownLink.displayName = 'DrilldownLink';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const HoverLink = styled(StyledBaseLink)`
   text-decoration: none;
 
@@ -203,6 +223,8 @@ export const HoverLink = styled(StyledBaseLink)`
 `;
 HoverLink.displayName = 'HoverLink';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const LinkBox = styled(StyledBaseLink)`
   text-decoration: none;
 
@@ -215,6 +237,8 @@ export const LinkBox = styled(StyledBaseLink)`
 `;
 LinkBox.displayName = 'LinkBox';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const DiscreetLinkBox = styled(StyledBaseLink)`
   text-decoration: none;
 
@@ -229,11 +253,15 @@ export const DiscreetLinkBox = styled(StyledBaseLink)`
 `;
 LinkBox.displayName = 'DiscreetLinkBox';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const DiscreetLink = styled(HoverLink)`
   --border: ${themeBorder('default', 'linkDiscreet')};
 `;
 DiscreetLink.displayName = 'DiscreetLink';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const ContentLink = styled(HoverLink)`
   --color: ${themeColor('pageTitle')};
   --border: ${themeBorder('default', 'contentLinkBorder')};
@@ -249,6 +277,8 @@ export const ContentLink = styled(HoverLink)`
 `;
 ContentLink.displayName = 'ContentLink';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const StandoutLink = styled(StyledBaseLink)`
   ${tw`sw-font-semibold`}
   ${tw`sw-no-underline`}
@@ -267,6 +297,8 @@ export const StandoutLink = styled(StyledBaseLink)`
 `;
 StandoutLink.displayName = 'StandoutLink';
 
+/** @deprecated Use either Link or LinkStandalone from Echoes instead.
+ */
 export const IssueIndicatorLink = styled(BaseLink)`
   color: ${themeColor('codeLineMeta')};
   text-decoration: none;
index adee7ea7bcfc0f5ab6367bcb70d1a45147474284..e98c67615ad95bcaedaee9459dbae7c7f3f7bb3a 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 styled from '@emotion/styled';
+import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
 import {
-  DiscreetLink,
   FlagMessage,
   LAYOUT_VIEWPORT_MIN_WIDTH,
   PageContentFontWrapper,
@@ -38,7 +39,7 @@ interface GlobalFooterProps {
   hideLoggedInInfo?: boolean;
 }
 
-export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
+export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooterProps>) {
   const appState = React.useContext(AppStateContext);
   const currentEdition = appState?.edition && getEdition(appState.edition);
 
@@ -53,7 +54,9 @@ export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
               <span className="sw-body-md-highlight">
                 {translate('footer.production_database_warning')}
               </span>
+
               <br />
+
               <InstanceMessage message={translate('footer.production_database_explanation')} />
             </p>
           </FlagMessage>
@@ -64,32 +67,51 @@ export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
 
           <ul className="sw-flex sw-items-center sw-gap-3 sw-ml-4">
             {!hideLoggedInInfo && currentEdition && <li>{currentEdition.name}</li>}
+
             {!hideLoggedInInfo && appState?.version && (
               <li className="sw-code">
                 {translateWithParameters('footer.version_x', appState.version)}
               </li>
             )}
+
             <li>
-              <DiscreetLink to="https://www.gnu.org/licenses/lgpl-3.0.txt">
+              <LinkStandalone
+                highlight={LinkHighlight.CurrentColor}
+                to="https://www.gnu.org/licenses/lgpl-3.0.txt"
+              >
                 {translate('footer.license')}
-              </DiscreetLink>
+              </LinkStandalone>
             </li>
+
             <li>
-              <DiscreetLink to="https://community.sonarsource.com/c/help/sq">
+              <LinkStandalone
+                highlight={LinkHighlight.CurrentColor}
+                to="https://community.sonarsource.com/c/help/sq"
+              >
                 {translate('footer.community')}
-              </DiscreetLink>
+              </LinkStandalone>
             </li>
+
             <li>
-              <DiscreetLink to={docUrl('/')}>{translate('footer.documentation')}</DiscreetLink>
+              <LinkStandalone highlight={LinkHighlight.CurrentColor} to={docUrl('/')}>
+                {translate('footer.documentation')}
+              </LinkStandalone>
             </li>
+
             <li>
-              <DiscreetLink to={docUrl('/instance-administration/plugin-version-matrix/')}>
+              <LinkStandalone
+                highlight={LinkHighlight.CurrentColor}
+                to={docUrl('/instance-administration/plugin-version-matrix/')}
+              >
                 {translate('footer.plugins')}
-              </DiscreetLink>
+              </LinkStandalone>
             </li>
+
             {!hideLoggedInInfo && (
               <li>
-                <DiscreetLink to="/web_api">{translate('footer.web_api')}</DiscreetLink>
+                <LinkStandalone highlight={LinkHighlight.CurrentColor} to="/web_api">
+                  {translate('footer.web_api')}
+                </LinkStandalone>
               </li>
             )}
           </ul>
@@ -100,8 +122,8 @@ export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
 }
 
 const StyledFooter = styled.div`
+  background-color: ${themeColor('backgroundSecondary')};
+  border-top: ${themeBorder('default')};
   box-sizing: border-box;
   min-width: ${LAYOUT_VIEWPORT_MIN_WIDTH}px;
-  border-top: ${themeBorder('default')};
-  background-color: ${themeColor('backgroundSecondary')};
 `;
index b9a5ddbf252e9206d9a91a33789fbd0789bcbcba..6c266081bd8de7348c98290caf4d21f22e8191e0 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { DiscreetLink } from 'design-system';
+
+import { Link, LinkHighlight } from '@sonarsource/echoes-react';
 import * as React from 'react';
 import { isOfficial } from '../../helpers/system';
 
@@ -27,21 +28,28 @@ export default function GlobalFooterBranding() {
   return official ? (
     <div>
       SonarQube&trade; technology is powered by{' '}
-      <DiscreetLink to="https://www.sonarsource.com">SonarSource SA</DiscreetLink>
+      <Link highlight={LinkHighlight.CurrentColor} to="https://www.sonarsource.com">
+        SonarSource SA
+      </Link>
     </div>
   ) : (
     <div>
       This application is based on{' '}
-      <DiscreetLink
+      <Link
+        highlight={LinkHighlight.CurrentColor}
         to="https://www.sonarsource.com/products/sonarqube/?referrer=sonarqube"
         title="SonarQube™"
       >
         SonarQube™
-      </DiscreetLink>{' '}
+      </Link>{' '}
       but is <strong>not</strong> an official version provided by{' '}
-      <DiscreetLink to="https://www.sonarsource.com" title="SonarSource SA">
+      <Link
+        highlight={LinkHighlight.CurrentColor}
+        to="https://www.sonarsource.com"
+        title="SonarSource SA"
+      >
         SonarSource SA
-      </DiscreetLink>
+      </Link>
       .
     </div>
   );
index 17d352d4c32569eb61ee7a171fae40314ffdbfe5..6684f2e5e7d5e4a09283ee96521edfffbb9fc8e5 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import {
-  ContentCell,
-  Key,
-  KeyboardHint,
-  Link,
-  Modal,
-  SubTitle,
-  Table,
-  TableRow,
-} from 'design-system';
+import { LinkStandalone } from '@sonarsource/echoes-react';
+import { ContentCell, Key, KeyboardHint, Modal, SubTitle, Table, TableRow } from 'design-system';
 import * as React from 'react';
 import { isInput } from '../../helpers/keyboardEventHelpers';
 import { KeyboardKeys } from '../../helpers/keycodes';
@@ -148,12 +140,14 @@ function renderSection() {
   return SECTIONS.map((section) => (
     <div key={section.subTitle} className="sw-mb-4">
       <SubTitle>{translate(section.subTitle)}</SubTitle>
+
       <Table columnCount={2} columnWidths={['30%', '70%']}>
         {section.rows.map((row) => (
           <TableRow key={row.command}>
             <ContentCell className="sw-justify-center">
               <KeyboardHint command={row.command} title="" />
             </ContentCell>
+
             <ContentCell>{translate(row.description)}</ContentCell>
           </TableRow>
         ))}
@@ -195,24 +189,25 @@ export default function KeyboardShortcutsModal() {
 
   const body = (
     <>
-      <Link
-        to="/account"
+      <LinkStandalone
         onClick={() => {
           setDisplay(false);
           return true;
         }}
+        to="/account"
       >
         {translate('keyboard_shortcuts_modal.disable_link')}
-      </Link>
+      </LinkStandalone>
+
       <div className="sw-mt-4">{renderSection()}</div>
     </>
   );
 
   return (
     <Modal
+      body={body}
       headerTitle={title}
       onClose={() => setDisplay(false)}
-      body={body}
       secondaryButtonLabel={translate('close')}
     />
   );
index 88fef8fc7972614a9a8566a827eca478d19c2c6a..08ceb637cafcf6230e7550d46d583a63f2f09348 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { HoverLink, TextMuted } from 'design-system';
+
+import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
 import * as React from 'react';
 import Favorite from '../../../../components/controls/Favorite';
 import { getComponentOverviewUrl } from '../../../../helpers/urls';
@@ -29,7 +30,7 @@ export interface BreadcrumbProps {
   currentUser: CurrentUser;
 }
 
-export function Breadcrumb(props: BreadcrumbProps) {
+export function Breadcrumb(props: Readonly<BreadcrumbProps>) {
   const { component, currentUser } = props;
 
   return (
@@ -48,15 +49,18 @@ export function Breadcrumb(props: BreadcrumbProps) {
                 qualifier={component.qualifier}
               />
             )}
-            <HoverLink
-              blurAfterClick
-              className="js-project-link sw-flex"
+
+            <LinkStandalone
+              highlight={LinkHighlight.Subdued}
+              className="js-project-link"
               key={breadcrumbElement.name}
+              shouldBlurAfterClick
               title={breadcrumbElement.name}
               to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)}
             >
-              <TextMuted text={breadcrumbElement.name} />
-            </HoverLink>
+              {breadcrumbElement.name}
+            </LinkStandalone>
+
             {isNotLast && <span className="slash-separator sw-mx-2" />}
           </div>
         );
index 5a2705da022f64d94e1ec833ba5d9d6f1f935872..4e8bbb639713a340722f819737e3155d8621b35f 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 { Link } from 'design-system';
+
+import { LinkStandalone } from '@sonarsource/echoes-react';
 import React from 'react';
 import { isPullRequest } from '../../../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../../helpers/system';
+import { isDefined } from '../../../../../helpers/types';
 import { AlmKeys } from '../../../../../types/alm-settings';
 import { BranchLike } from '../../../../../types/branch-like';
 import { Component } from '../../../../../types/types';
 
 function getPRUrlAlmKey(url = '') {
   const lowerCaseUrl = url.toLowerCase();
+
   if (lowerCaseUrl.includes(AlmKeys.GitHub)) {
     return AlmKeys.GitHub;
   } else if (lowerCaseUrl.includes(AlmKeys.GitLab)) {
@@ -41,29 +44,32 @@ function getPRUrlAlmKey(url = '') {
   ) {
     return AlmKeys.Azure;
   }
+
   return undefined;
 }
 
 export default function PRLink({
   currentBranchLike,
   component,
-}: {
+}: Readonly<{
   currentBranchLike: BranchLike;
   component: Component;
-}) {
+}>) {
   if (!isPullRequest(currentBranchLike)) {
     return null;
   }
 
   const almKey =
     component.alm?.key ||
-    (isPullRequest(currentBranchLike) && getPRUrlAlmKey(currentBranchLike.url));
+    (isPullRequest(currentBranchLike) && getPRUrlAlmKey(currentBranchLike.url)) ||
+    '';
+
   return (
     <>
-      {currentBranchLike.url !== undefined && (
-        <Link
-          icon={
-            almKey && (
+      {isDefined(currentBranchLike.url) && (
+        <LinkStandalone
+          iconLeft={
+            almKey !== '' && (
               <img
                 alt={almKey}
                 height={16}
@@ -75,8 +81,8 @@ export default function PRLink({
           key={currentBranchLike.key}
           to={currentBranchLike.url}
         >
-          {!almKey && translate('branches.see_the_pr')}
-        </Link>
+          {almKey === '' && translate('branches.see_the_pr')}
+        </LinkStandalone>
       )}
     </>
   );
index 12e71ce081de1803cb4accc03cea92da27237eeb..8d56f7b1cdc6b8aa98a671be1018e1158e6603dc 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
+import { LinkStandalone } from '@sonarsource/echoes-react';
 import {
   ButtonPrimary,
   Card,
@@ -25,7 +27,6 @@ import {
   FlagMessage,
   FormField,
   InputField,
-  Link,
   PageContentFontWrapper,
   Spinner,
   SubTitle,
@@ -40,28 +41,30 @@ import Unauthorized from '../sessions/components/Unauthorized';
 import { DEFAULT_ADMIN_PASSWORD } from './constants';
 
 export interface ChangeAdminPasswordAppRendererProps {
-  passwordValue: string;
+  canAdmin?: boolean;
+  canSubmit?: boolean;
   confirmPasswordValue: string;
+  location: Location;
   onConfirmPasswordChange: (password: string) => void;
   onPasswordChange: (password: string) => void;
   onSubmit: () => void;
-  canAdmin?: boolean;
-  canSubmit?: boolean;
+  passwordValue: string;
   submitting: boolean;
   success: boolean;
-  location: Location;
 }
 
 const PASSWORD_FIELD_ID = 'user-password';
 const CONFIRM_PASSWORD_FIELD_ID = 'confirm-user-password';
 
-export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswordAppRendererProps) {
+export default function ChangeAdminPasswordAppRenderer(
+  props: Readonly<ChangeAdminPasswordAppRendererProps>,
+) {
   const {
     canAdmin,
     canSubmit,
     confirmPasswordValue,
-    passwordValue,
     location,
+    passwordValue,
     submitting,
     success,
   } = props;
@@ -73,24 +76,28 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
   return (
     <CenteredLayout>
       <Helmet defer={false} title={translate('users.change_admin_password.page')} />
+
       <PageContentFontWrapper className="sw-body-sm sw-flex sw-flex-col sw-items-center sw-justify-center">
         <Card className="sw-mx-auto sw-mt-24 sw-w-abs-600 sw-flex sw-items-stretch sw-flex-col">
           {success ? (
             <FlagMessage className="sw-my-8" variant="success">
               <div>
                 <p className="sw-mb-2">{translate('users.change_admin_password.form.success')}</p>
+
                 {/* We must reload because we need a refresh of the /api/navigation/global call. */}
-                <Link to={getReturnUrl(location)} reloadDocument>
+                <LinkStandalone to={getReturnUrl(location)} reloadDocument>
                   {translate('users.change_admin_password.form.continue_to_app')}
-                </Link>
+                </LinkStandalone>
               </div>
             </FlagMessage>
           ) : (
             <>
               <Title>{translate('users.change_admin_password.instance_is_at_risk')}</Title>
+
               <DarkLabel className="sw-mb-2">
                 {translate('users.change_admin_password.header')}
               </DarkLabel>
+
               <p>{translate('users.change_admin_password.description')}</p>
 
               <form
@@ -105,8 +112,8 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
                 </SubTitle>
 
                 <FormField
-                  label={translate('users.change_admin_password.form.password')}
                   htmlFor={PASSWORD_FIELD_ID}
+                  label={translate('users.change_admin_password.form.password')}
                   required
                 >
                   <InputField
@@ -122,9 +129,6 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
                 </FormField>
 
                 <FormField
-                  label={translate('users.change_admin_password.form.confirm')}
-                  htmlFor={CONFIRM_PASSWORD_FIELD_ID}
-                  required
                   description={
                     confirmPasswordValue === passwordValue &&
                     passwordValue === DEFAULT_ADMIN_PASSWORD && (
@@ -133,6 +137,9 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
                       </FlagMessage>
                     )
                   }
+                  htmlFor={CONFIRM_PASSWORD_FIELD_ID}
+                  label={translate('users.change_admin_password.form.confirm')}
+                  required
                 >
                   <InputField
                     id={CONFIRM_PASSWORD_FIELD_ID}
@@ -152,6 +159,7 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
                   type="submit"
                 >
                   <Spinner className="sw-mr-2" loading={submitting} />
+
                   {translate('update_verb')}
                 </ButtonPrimary>
               </form>
index 4f6ad03db36ebd32b070409853f7e0fb9ca6a7f5..5680c74944d5662587234a0f9538fff1d3fefc3f 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 { Badge, BranchIcon, HoverLink, LightLabel, Note, QualifierIcon } from 'design-system';
+
+import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
+import { Badge, BranchIcon, LightLabel, Note, QualifierIcon } from 'design-system';
 import * as React from 'react';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
 import { CodeScope, getComponentOverviewUrl, queryToSearch } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 import {
@@ -37,8 +40,8 @@ export function getTooltip(component: ComponentMeasure) {
     component.qualifier === ComponentQualifier.File ||
     component.qualifier === ComponentQualifier.TestFile;
 
-  if (isFile && component.path) {
-    return component.path + '\n\n' + component.key;
+  if (isFile && isDefined(component.path)) {
+    return `${component.path}\n\n${component.key}`;
   }
 
   return [component.name, component.key, component.branch].filter((s) => !!s).join('\n\n');
@@ -48,23 +51,23 @@ export interface Props {
   branchLike?: BranchLike;
   canBrowse?: boolean;
   component: ComponentMeasure;
+  newCodeSelected?: boolean;
   previous?: ComponentMeasure;
   rootComponent: ComponentMeasure;
-  unclickable?: boolean;
-  newCodeSelected?: boolean;
   showIcon?: boolean;
+  unclickable?: boolean;
 }
 
 export default function ComponentName({
   branchLike,
-  component,
-  unclickable = false,
-  rootComponent,
-  previous,
   canBrowse = false,
+  component,
   newCodeSelected,
+  previous,
+  rootComponent,
   showIcon = true,
-}: Props) {
+  unclickable = false,
+}: Readonly<Props>) {
   const ariaLabel = unclickable ? translate('code.parent_folder') : undefined;
 
   if (
@@ -89,9 +92,11 @@ export default function ComponentName({
             newCodeSelected,
           )}
         </div>
+
         {component.branch ? (
           <div className="sw-truncate sw-ml-2">
             <BranchIcon className="sw-mr-1" />
+
             <Note>{component.branch}</Note>
           </div>
         ) : (
@@ -102,6 +107,7 @@ export default function ComponentName({
       </span>
     );
   }
+
   return (
     <span title={getTooltip(component)} aria-label={ariaLabel}>
       {renderNameWithIcon(
@@ -129,6 +135,7 @@ function renderNameWithIcon(
 ) {
   const name = renderName(component, previous);
   const codeType = newCodeSelected ? CodeScope.New : CodeScope.Overall;
+
   if (
     !unclickable &&
     (isPortfolioLike(component.qualifier) ||
@@ -140,9 +147,11 @@ function renderNameWithIcon(
     )
       ? component.branch
       : undefined;
+
     return (
-      <HoverLink
-        icon={showIcon && <QualifierIcon className="sw-mr-1" qualifier={component.qualifier} />}
+      <LinkStandalone
+        highlight={LinkHighlight.CurrentColor}
+        iconLeft={showIcon && <QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
         to={getComponentOverviewUrl(
           component.refKey ?? component.key,
           component.qualifier,
@@ -151,27 +160,32 @@ function renderNameWithIcon(
         )}
       >
         {name}
-      </HoverLink>
+      </LinkStandalone>
     );
   } else if (canBrowse) {
     const query = { id: rootComponent.key, ...getBranchLikeQuery(branchLike) };
+
     if (component.key !== rootComponent.key) {
       Object.assign(query, { selected: component.key });
     }
+
     return (
-      <HoverLink
-        icon={showIcon && <QualifierIcon qualifier={component.qualifier} />}
+      <LinkStandalone
+        highlight={LinkHighlight.CurrentColor}
+        iconLeft={showIcon && <QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
         to={{ pathname: '/code', search: queryToSearch(query) }}
       >
         {name}
-      </HoverLink>
+      </LinkStandalone>
     );
   }
+
   return (
     <span>
       {showIcon && (
-        <QualifierIcon className="sw-mr-1 sw-align-text-bottom" qualifier={component.qualifier} />
+        <QualifierIcon className="sw-mr-2 sw-align-text-bottom" qualifier={component.qualifier} />
       )}
+
       {name}
     </span>
   );
@@ -182,13 +196,16 @@ function renderName(component: ComponentMeasure, previous: ComponentMeasure | un
     component.qualifier === ComponentQualifier.Directory &&
     previous &&
     previous.qualifier === ComponentQualifier.Directory;
+
   const prefix =
-    areBothDirs && previous !== undefined
+    areBothDirs && isDefined(previous)
       ? mostCommonPrefix([component.name + '/', previous.name + '/'])
       : '';
+
   return prefix ? (
     <span>
       <LightLabel>{prefix}</LightLabel>
+
       <span>{component.name.slice(prefix.length)}</span>
     </span>
   ) : (
index 03f90e84fe0cf3fa5d411320fae71d3cb15864fe..784d2cc1f97dfdc4b61f291dcc34ebabf2b415f2 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 styled from '@emotion/styled';
-import { ButtonPrimary, Card, CenteredLayout, Link, Note, Spinner, Title } from 'design-system';
+import { Link, LinkStandalone, Spinner } from '@sonarsource/echoes-react';
+import { ButtonPrimary, Card, CenteredLayout, Note, Title } from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { FormattedMessage } from 'react-intl';
@@ -29,6 +31,7 @@ import DateFromNow from '../../../components/intl/DateFromNow';
 import TimeFormatter from '../../../components/intl/TimeFormatter';
 import { translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
+import { isDefined } from '../../../helpers/types';
 import { getReturnUrl } from '../../../helpers/urls';
 
 interface Props {
@@ -59,13 +62,15 @@ export default class App extends React.PureComponent<Props, State> {
 
   componentWillUnmount() {
     this.mounted = false;
-    if (this.interval !== undefined) {
+
+    if (isDefined(this.interval)) {
       window.clearInterval(this.interval);
     }
   }
 
   fetchStatus = () => {
     const request = this.props.setup ? this.fetchMigrationState() : this.fetchSystemStatus();
+
     request.catch(() => {
       if (this.mounted) {
         this.setState({
@@ -137,6 +142,7 @@ export default class App extends React.PureComponent<Props, State> {
     return (
       <>
         <Helmet defaultTitle={translate('maintenance.page')} defer={false} />
+
         <CenteredLayout className="sw-flex sw-justify-around sw-mt-32" id="bd">
           <Card className="sw-body-sm sw-p-10 sw-w-abs-400" id="nonav">
             {status === 'OFFLINE' && (
@@ -144,13 +150,15 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle className="text-danger">
                   <InstanceMessage message={translate('maintenance.is_offline')} />
                 </MaintenanceTitle>
+
                 <MaintenanceText>
                   {translate('maintenance.sonarqube_is_offline.text')}
                 </MaintenanceText>
+
                 <div className="sw-text-center">
-                  <Link reloadDocument to={`${getBaseUrl()}/`}>
+                  <LinkStandalone reloadDocument to={`${getBaseUrl()}/`}>
                     {translate('maintenance.try_again')}
-                  </Link>
+                  </LinkStandalone>
                 </div>
               </>
             )}
@@ -160,11 +168,13 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle>
                   <InstanceMessage message={translate('maintenance.is_up')} />
                 </MaintenanceTitle>
+
                 <MaintenanceText className="sw-text-center">
                   {translate('maintenance.all_systems_opetational')}
                 </MaintenanceText>
+
                 <div className="sw-text-center">
-                  <Link to="/">{translate('layout.home')}</Link>
+                  <LinkStandalone to="/">{translate('layout.home')}</LinkStandalone>
                 </div>
               </>
             )}
@@ -174,6 +184,7 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle>
                   <InstanceMessage message={translate('maintenance.is_starting')} />
                 </MaintenanceTitle>
+
                 <MaintenanceSpinner>
                   <Spinner />
                 </MaintenanceSpinner>
@@ -185,11 +196,13 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle className="text-danger">
                   <InstanceMessage message={translate('maintenance.is_down')} />
                 </MaintenanceTitle>
+
                 <MaintenanceText>{translate('maintenance.sonarqube_is_down.text')}</MaintenanceText>
+
                 <MaintenanceText className="sw-text-center">
-                  <Link reloadDocument to={`${getBaseUrl()}/`}>
+                  <LinkStandalone reloadDocument to={`${getBaseUrl()}/`}>
                     {translate('maintenance.try_again')}
-                  </Link>
+                  </LinkStandalone>
                 </MaintenanceText>
               </>
             )}
@@ -199,22 +212,21 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle>
                   <InstanceMessage message={translate('maintenance.is_under_maintenance')} />
                 </MaintenanceTitle>
+
                 <MaintenanceText>
                   <FormattedMessage
                     defaultMessage={translate('maintenance.sonarqube_is_under_maintenance.1')}
                     id="maintenance.sonarqube_is_under_maintenance.1"
                     values={{
                       link: (
-                        <Link
-                          to="https://www.sonarlint.org/?referrer=sonarqube-maintenance"
-                          target="_blank"
-                        >
+                        <Link to="https://www.sonarlint.org/?referrer=sonarqube-maintenance">
                           {translate('maintenance.sonarqube_is_under_maintenance_link.1')}
                         </Link>
                       ),
                     }}
                   />
                 </MaintenanceText>
+
                 <MaintenanceText>
                   <FormattedMessage
                     defaultMessage={translate('maintenance.sonarqube_is_under_maintenance.2')}
@@ -236,8 +248,9 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle>
                   {translate('maintenance.database_is_up_to_date')}
                 </MaintenanceTitle>
+
                 <div className="sw-text-center">
-                  <Link to="/">{translate('layout.home')}</Link>
+                  <LinkStandalone to="/">{translate('layout.home')}</LinkStandalone>
                 </div>
               </>
             )}
@@ -245,9 +258,13 @@ export default class App extends React.PureComponent<Props, State> {
             {state === 'MIGRATION_REQUIRED' && (
               <>
                 <MaintenanceTitle>{translate('maintenance.upgrade_database')}</MaintenanceTitle>
+
                 <MaintenanceText>{translate('maintenance.upgrade_database.1')}</MaintenanceText>
+
                 <MaintenanceText>{translate('maintenance.upgrade_database.2')}</MaintenanceText>
+
                 <MaintenanceText>{translate('maintenance.upgrade_database.3')}</MaintenanceText>
+
                 <MaintenanceSpinner>
                   <ButtonPrimary id="start-migration" onClick={this.handleMigrateClick}>
                     {translate('maintenance.upgrade')}
@@ -261,6 +278,7 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle className="text-danger">
                   {translate('maintenance.migration_not_supported')}
                 </MaintenanceTitle>
+
                 <p>{translate('maintenance.migration_not_supported.text')}</p>
               </>
             )}
@@ -268,10 +286,12 @@ export default class App extends React.PureComponent<Props, State> {
             {state === 'MIGRATION_RUNNING' && (
               <>
                 <MaintenanceTitle>{translate('maintenance.database_migration')}</MaintenanceTitle>
-                {this.state.message !== undefined && (
+
+                {isDefined(this.state.message) && (
                   <MaintenanceText className="sw-text-center">{this.state.message}</MaintenanceText>
                 )}
-                {this.state.startedAt !== undefined && (
+
+                {isDefined(this.state.startedAt) && (
                   <MaintenanceText className="sw-text-center">
                     {translate('background_tasks.table.started')}{' '}
                     <DateFromNow date={this.state.startedAt} />
@@ -281,6 +301,7 @@ export default class App extends React.PureComponent<Props, State> {
                     </Note>
                   </MaintenanceText>
                 )}
+
                 <MaintenanceSpinner>
                   <Spinner />
                 </MaintenanceSpinner>
@@ -292,8 +313,9 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle className="text-success">
                   {translate('maintenance.database_is_up_to_date')}
                 </MaintenanceTitle>
+
                 <div className="sw-text-center">
-                  <Link to="/">{translate('layout.home')}</Link>
+                  <LinkStandalone to="/">{translate('layout.home')}</LinkStandalone>
                 </div>
               </>
             )}
@@ -303,6 +325,7 @@ export default class App extends React.PureComponent<Props, State> {
                 <MaintenanceTitle className="text-danger">
                   {translate('maintenance.upgrade_failed')}
                 </MaintenanceTitle>
+
                 <MaintenanceText>{translate('maintenance.upgrade_failed.text')}</MaintenanceText>
               </>
             )}
@@ -314,8 +337,8 @@ export default class App extends React.PureComponent<Props, State> {
 }
 
 const MaintenanceTitle = styled(Title)`
-  text-align: center;
   margin-bottom: 2.5rem;
+  text-align: center;
 `;
 
 const MaintenanceText = styled.p`
index df14b944f1c0f853e9aafdd954e2cd5cfb427e73..ce9504100f4a2732a27d441e7161a9f07ca9c178 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 { Link, Spinner } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
-import { FlagMessage, Link, Spinner } from 'design-system';
+import { FlagMessage } from 'design-system';
 import * as React from 'react';
 import { useComponent } from '../../../app/components/componentContext/withComponentContext';
 import { translate } from '../../../helpers/l10n';
@@ -29,27 +31,30 @@ import { AnalysisErrorModal } from './AnalysisErrorModal';
 import AnalysisWarningsModal from './AnalysisWarningsModal';
 
 export interface HeaderMetaProps {
-  component: Component;
   className?: string;
+  component: Component;
 }
 
-export function AnalysisStatus(props: HeaderMetaProps) {
+export function AnalysisStatus(props: Readonly<HeaderMetaProps>) {
   const { className, component } = props;
   const { currentTask, isPending, isInProgress } = useComponent();
   const { data: warnings, isLoading } = useBranchWarningQuery(component);
 
   const [modalIsVisible, setDisplayModal] = React.useState(false);
+
   const openModal = React.useCallback(() => {
     setDisplayModal(true);
   }, [setDisplayModal]);
+
   const closeModal = React.useCallback(() => {
     setDisplayModal(false);
   }, [setDisplayModal]);
 
   if (isInProgress || isPending) {
     return (
-      <div data-test="analysis-status" className={classNames('sw-flex sw-items-center', className)}>
+      <div className={classNames('sw-flex sw-items-center', className)} data-test="analysis-status">
         <Spinner />
+
         <span className="sw-ml-1">
           {isInProgress
             ? translate('project_navigation.analysis_status.in_progress')
@@ -62,12 +67,22 @@ export function AnalysisStatus(props: HeaderMetaProps) {
   if (currentTask?.status === TaskStatuses.Failed) {
     return (
       <>
-        <FlagMessage data-test="analysis-status" variant="error" className={className}>
+        <FlagMessage className={className} data-test="analysis-status" variant="error">
           <span>{translate('project_navigation.analysis_status.failed')}</span>
-          <Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
+
+          {/* TODO: replace the Link below with a lighweight/discreet button component */}
+          {/* when it is available in Echoes */}
+          <Link
+            className="sw-ml-1"
+            onClick={openModal}
+            shouldBlurAfterClick
+            shouldPreventDefault
+            to={{}}
+          >
             {translate('project_navigation.analysis_status.details_link')}
           </Link>
         </FlagMessage>
+
         {modalIsVisible && (
           <AnalysisErrorModal
             component={component}
@@ -82,12 +97,22 @@ export function AnalysisStatus(props: HeaderMetaProps) {
   if (!isLoading && warnings && warnings.length > 0) {
     return (
       <>
-        <FlagMessage data-test="analysis-status" variant="warning" className={className}>
+        <FlagMessage className={className} data-test="analysis-status" variant="warning">
           <span>{translate('project_navigation.analysis_status.warnings')}</span>
-          <Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
+
+          {/* TODO: replace the Link below with a lighweight/discreet button component */}
+          {/* when it is available in Echoes */}
+          <Link
+            className="sw-ml-1"
+            onClick={openModal}
+            shouldBlurAfterClick
+            shouldPreventDefault
+            to={{}}
+          >
             {translate('project_navigation.analysis_status.details_link')}
           </Link>
         </FlagMessage>
+
         {modalIsVisible && (
           <AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
         )}
index 930793f377bddbb4052e712fdb75a435d8c4cca5..9713e6484f68de1dfd98810db57e9a428b28d997 100644 (file)
@@ -17,7 +17,9 @@
  * 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 { Link, LinkStandalone } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
 import {
   Badge,
@@ -27,7 +29,6 @@ import {
   Note,
   QualityGateIndicator,
   SeparatorCircleIcon,
-  StandoutLink,
   SubnavigationFlowSeparator,
   Tags,
   themeBorder,
@@ -42,6 +43,7 @@ import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
 import Measure from '../../../../components/measure/Measure';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { formatMeasure } from '../../../../helpers/measures';
+import { isDefined } from '../../../../helpers/types';
 import { getProjectUrl } from '../../../../helpers/urls';
 import { ComponentQualifier } from '../../../../types/component';
 import { MetricKey, MetricType } from '../../../../types/metrics';
@@ -70,7 +72,7 @@ function renderFirstLine(
     <>
       <div className="sw-flex sw-justify-between sw-items-center ">
         <div className="sw-flex sw-items-center ">
-          {isFavorite !== undefined && (
+          {isDefined(isFavorite) && (
             <Favorite
               className="sw-mr-2"
               component={key}
@@ -82,7 +84,7 @@ function renderFirstLine(
           )}
 
           <span className="it__project-card-name" title={name}>
-            <StandoutLink to={getProjectUrl(key)}>{name}</StandoutLink>
+            <LinkStandalone to={getProjectUrl(key)}>{name}</LinkStandalone>
           </span>
 
           {qualifier === ComponentQualifier.Application && (
@@ -90,7 +92,7 @@ function renderFirstLine(
               overlay={
                 <span>
                   {translate('qualifier.APP')}
-                  {measures.projects && (
+                  {measures.projects !== '' && (
                     <span>
                       {' ‒ '}
                       {translateWithParameters('x_projects_', measures.projects)}
@@ -111,7 +113,8 @@ function renderFirstLine(
             </span>
           </Tooltip>
         </div>
-        {analysisDate && (
+
+        {isDefined(analysisDate) && analysisDate !== '' && (
           <Tooltip overlay={qualityGateLabel}>
             <span className="sw-flex sw-items-center">
               <QualityGateIndicator
@@ -123,8 +126,9 @@ function renderFirstLine(
           </Tooltip>
         )}
       </div>
+
       <LightLabel as="div" className="sw-flex sw-items-center sw-mt-3">
-        {analysisDate && (
+        {isDefined(analysisDate) && analysisDate !== '' && (
           <DateTimeFormatter date={analysisDate}>
             {(formattedAnalysisDate) => (
               <span className="sw-body-sm-highlight" title={formattedAnalysisDate}>
@@ -139,10 +143,12 @@ function renderFirstLine(
             )}
           </DateTimeFormatter>
         )}
+
         {isNewCode
           ? measures[MetricKey.new_lines] != null && (
               <>
                 <SeparatorCircleIcon className="sw-mx-1" />
+
                 <div>
                   <span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.new_lines}>
                     <Measure
@@ -151,6 +157,7 @@ function renderFirstLine(
                       value={measures.new_lines}
                     />
                   </span>
+
                   <span className="sw-body-sm">{translate('metric.new_lines.name')}</span>
                 </div>
               </>
@@ -158,6 +165,7 @@ function renderFirstLine(
           : measures[MetricKey.ncloc] != null && (
               <>
                 <SeparatorCircleIcon className="sw-mx-1" />
+
                 <div>
                   <span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.ncloc}>
                     <Measure
@@ -166,17 +174,22 @@ function renderFirstLine(
                       value={measures.ncloc}
                     />
                   </span>
+
                   <span className="sw-body-sm">{translate('metric.ncloc.name')}</span>
                 </div>
+
                 <SeparatorCircleIcon className="sw-mx-1" />
+
                 <span className="sw-body-sm" data-key={MetricKey.ncloc_language_distribution}>
                   <ProjectCardLanguages distribution={measures.ncloc_language_distribution} />
                 </span>
               </>
             )}
+
         {tags.length > 0 && (
           <>
             <SeparatorCircleIcon className="sw-mx-1" />
+
             <Tags
               className="sw-body-sm"
               emptyText={translate('issue.no_tag')}
@@ -199,7 +212,11 @@ function renderSecondLine(
 ) {
   const { analysisDate, key, leakPeriodDate, measures, qualifier, isScannable } = project;
 
-  if (analysisDate && (!isNewCode || leakPeriodDate)) {
+  if (
+    isDefined(analysisDate) &&
+    analysisDate !== '' &&
+    (!isNewCode || (isDefined(leakPeriodDate) && leakPeriodDate !== ''))
+  ) {
     return (
       <ProjectCardMeasures
         measures={measures}
@@ -216,19 +233,20 @@ function renderSecondLine(
           ? translate('projects.no_new_code_period', qualifier)
           : translate('projects.not_analyzed', qualifier)}
       </Note>
+
       {qualifier !== ComponentQualifier.Application &&
-        !analysisDate &&
+        (analysisDate === undefined || analysisDate === '') &&
         isLoggedIn(currentUser) &&
         isScannable && (
-          <StandoutLink className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
+          <Link className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
             {translate('projects.configure_analysis')}
-          </StandoutLink>
+          </Link>
         )}
     </div>
   );
 }
 
-export default function ProjectCard(props: Props) {
+export default function ProjectCard(props: Readonly<Props>) {
   const { currentUser, type, project } = props;
   const isNewCode = type === 'leak';
 
@@ -240,7 +258,9 @@ export default function ProjectCard(props: Props) {
       data-key={project.key}
     >
       {renderFirstLine(project, props.handleFavorite, isNewCode)}
+
       <SubnavigationFlowSeparator className="sw-my-3" />
+
       {renderSecondLine(currentUser, project, isNewCode)}
     </ProjectCardWrapper>
   );
index 6b0b3606b601f0095e13ef60119f9d675c9a1bca..22af0695448469455dcf8ff6b05c8ed3b1b67c93 100644 (file)
@@ -17,7 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { FlagMessage, Link, SubTitle } from 'design-system';
+
+import { LinkStandalone } from '@sonarsource/echoes-react';
+import { FlagMessage, SubTitle } from 'design-system';
 import * as React from 'react';
 import { getQualityProfileExporterUrl } from '../../../api/quality-profiles';
 import { translate } from '../../../helpers/l10n';
@@ -28,7 +30,7 @@ interface Props {
   profile: Profile;
 }
 
-export default function ProfileExporters({ exporters, profile }: Props) {
+export default function ProfileExporters({ exporters, profile }: Readonly<Props>) {
   const exportersForLanguage = exporters.filter((e) => e.languages.includes(profile.language));
 
   if (exportersForLanguage.length === 0) {
@@ -40,15 +42,21 @@ export default function ProfileExporters({ exporters, profile }: Props) {
       <div>
         <SubTitle>{translate('quality_profiles.exporters')}</SubTitle>
       </div>
+
       <FlagMessage className="sw-mb-4" variant="warning">
         {translate('quality_profiles.exporters.deprecated')}
       </FlagMessage>
+
       <ul className="sw-flex sw-flex-col sw-gap-2">
         {exportersForLanguage.map((exporter) => (
           <li data-key={exporter.key} key={exporter.key}>
-            <Link isExternal showExternalIcon to={getQualityProfileExporterUrl(exporter, profile)}>
+            <LinkStandalone
+              hasExternalIcon
+              isExternal
+              to={getQualityProfileExporterUrl(exporter, profile)}
+            >
               {exporter.name}
-            </Link>
+            </LinkStandalone>
           </li>
         ))}
       </ul>
index 62126f3b35a08d58f6c2d701e56a43ae2c51f560..6fc42721e65879c8fcad733c9cb6710f5fa76f99 100644 (file)
@@ -233,8 +233,6 @@ it('should show About page', async () => {
   await user.click(ui.apiScopePet.get());
   await user.click(ui.apiSidebarItem.getAt(0));
   expect(screen.queryByText('about')).not.toBeInTheDocument();
-  await user.click(ui.title.get());
-  expect(await screen.findByText('about')).toBeInTheDocument();
 });
 
 function renderWebApiApp() {
index e867ad8db0787068854f1640b4dbcfd6f4d1fb27..39f3f0bf5d28052ae3ede132de9e44799f1c2642 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 {
   Badge,
   BasicSeparator,
   Checkbox,
   HelperHintIcon,
   InputSearch,
-  Link,
   SubnavigationAccordion,
   SubnavigationItem,
   SubnavigationSubheading,
@@ -41,13 +41,14 @@ import ApiFilterContext from './ApiFilterContext';
 import RestMethodPill from './RestMethodPill';
 
 interface Api {
-  name: string;
-  method: string;
   info: OpenAPIV3.OperationObject<InternalExtension>;
+  method: string;
+  name: string;
 }
+
 interface Props {
-  docInfo: OpenAPIV3.InfoObject;
   apisList: Api[];
+  docInfo: OpenAPIV3.InfoObject;
 }
 
 const METHOD_ORDER: Dict<number> = {
@@ -82,6 +83,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
         .filter((api) => showInternal || !api.info['x-internal'])
         .reduce<Record<string, Api[]>>((acc, api) => {
           const subgroup = api.name.split('/')[1];
+
           return {
             ...acc,
             [subgroup]: [...(acc[subgroup] ?? []), api],
@@ -92,16 +94,12 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
 
   return (
     <>
-      <h1 className="sw-mb-2">
-        <Link to="." className="sw-text-[unset] sw-border-none">
-          {docInfo.title}
-        </Link>
-      </h1>
+      <h1 className="sw-mb-2">{docInfo.title}</h1>
 
       <InputSearch
         className="sw-w-full"
-        placeholder={translate('api_documentation.v2.search')}
         onChange={setSearch}
+        placeholder={translate('api_documentation.v2.search')}
         value={search}
       />
 
@@ -109,6 +107,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
         <Checkbox checked={showInternal} onCheck={() => setShowInternal((prev) => !prev)}>
           <span className="sw-ml-2">{translate('api_documentation.show_internal_v2')}</span>
         </Checkbox>
+
         <HelpTooltip
           className="sw-ml-2"
           overlay={translate('api_documentation.internal_tooltip_v2')}
@@ -119,27 +118,31 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
 
       {Object.entries(groupedList).map(([group, apis]) => (
         <SubnavigationAccordion
+          className="sw-mt-2"
+          header={group}
+          id={`web-api-${group}`}
           initExpanded={apis.some(
             ({ name, method }) => name === activeApi[0] && method === activeApi[1],
           )}
-          className="sw-mt-2"
-          header={group}
           key={group}
-          id={`web-api-${group}`}
         >
           {sortBy(apis, (a) => [a.name, METHOD_ORDER[a.method]]).map(
             ({ method, name, info }, index, sorted) => {
               const resourceName = getResourceFromName(name);
+
               const previousResourceName =
                 index > 0 ? getResourceFromName(sorted[index - 1].name) : undefined;
+
               const isNewResource = resourceName !== previousResourceName;
 
               return (
                 <Fragment key={getApiEndpointKey(name, method)}>
                   {index > 0 && isNewResource && <BasicSeparator />}
+
                   {(index === 0 || isNewResource) && (
                     <SubnavigationSubheading>{resourceName}</SubnavigationSubheading>
                   )}
+
                   <SubnavigationItem
                     active={name === activeApi[0] && method === activeApi[1]}
                     onClick={handleApiClick}
@@ -148,6 +151,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
                     <div className="sw-flex sw-gap-2 sw-w-full sw-justify-between">
                       <div className="sw-flex sw-gap-2">
                         <RestMethodPill method={method} />
+
                         <div>{info.summary ?? name}</div>
                       </div>
 
@@ -158,6 +162,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
                               {translate('internal')}
                             </Badge>
                           )}
+
                           {info.deprecated && (
                             <Badge variant="deleted" className="sw-self-center">
                               {translate('deprecated')}
index a2b684aef31a31e9031fa39e98bcfa0411741c7b..07fe0ed08d44cc91148037059d41256311d04ad4 100644 (file)
@@ -17,7 +17,9 @@
  * 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 { LinkStandalone } from '@sonarsource/echoes-react';
 import {
   ClipboardIconButton,
   DrilldownLink,
@@ -25,7 +27,6 @@ import {
   InteractiveIcon,
   ItemButton,
   ItemLink,
-  Link,
   MenuIcon,
   Note,
   PopupPlacement,
@@ -44,20 +45,19 @@ import { formatMeasure } from '../../helpers/measures';
 import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
 import { omitNil } from '../../helpers/request';
 import { getBaseUrl } from '../../helpers/system';
+import { isDefined } from '../../helpers/types';
 import {
   getBranchLikeUrl,
   getCodeUrl,
   getComponentIssuesUrl,
   getComponentSecurityHotspotsUrl,
 } from '../../helpers/urls';
-import { DEFAULT_ISSUES_QUERY } from '../shared/utils';
-
+import type { BranchLike } from '../../types/branch-like';
 import { ComponentQualifier } from '../../types/component';
 import { IssueType } from '../../types/issues';
 import { MetricKey, MetricType } from '../../types/metrics';
-
-import type { BranchLike } from '../../types/branch-like';
 import type { Measure, SourceViewerFile } from '../../types/types';
+import { DEFAULT_ISSUES_QUERY } from '../shared/utils';
 import type { WorkspaceContextShape } from '../workspace/context';
 
 interface Props {
@@ -142,19 +142,22 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
       >
         <div className="sw-flex sw-flex-1 sw-flex-col sw-gap-1 sw-mr-5 sw-my-1">
           <div className="sw-flex sw-gap-1 sw-items-center">
-            <Link icon={<ProjectIcon />} to={getBranchLikeUrl(project, this.props.branchLike)}>
+            <LinkStandalone
+              iconLeft={<ProjectIcon className="sw-mr-2" />}
+              to={getBranchLikeUrl(project, this.props.branchLike)}
+            >
               {projectName}
-            </Link>
+            </LinkStandalone>
           </div>
 
-          <div className="sw-flex sw-gap-1 sw-items-center">
+          <div className="sw-flex sw-gap-2 sw-items-center">
             <QualifierIcon qualifier={q} />
 
             {collapsedDirFromPath(path)}
 
             {fileFromPath(path)}
 
-            <span className="sw-ml-1">
+            <span>
               <ClipboardIconButton
                 aria-label={translate('component_viewer.copy_path_to_clipboard')}
                 copyValue={path}
@@ -165,7 +168,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
 
         {showMeasures && (
           <div className="sw-flex sw-gap-6 sw-items-center">
-            {measures[unitTestsOrLines] && (
+            {isDefined(measures[unitTestsOrLines]) && (
               <div className="sw-flex sw-flex-col sw-gap-1">
                 <Note className="it__source-viewer-header-measure-label sw-body-lg">
                   {translate(`metric.${unitTestsOrLines}.name`)}
@@ -177,7 +180,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
               </div>
             )}
 
-            {measures.coverage !== undefined && (
+            {isDefined(measures.coverage) && (
               <div className="sw-flex sw-flex-col sw-gap-1">
                 <Note className="it__source-viewer-header-measure-label sw-body-lg">
                   {translate('metric.coverage.name')}
@@ -189,7 +192,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
               </div>
             )}
 
-            {measures.duplicationDensity !== undefined && (
+            {isDefined(measures.duplicationDensity) && (
               <div className="sw-flex sw-flex-col sw-gap-1">
                 <Note className="it__source-viewer-header-measure-label sw-body-lg">
                   {translate('duplications')}
index aa9e5b0b9f152da7b01cc55c74481a33313028ca..906b9af74f682c25c191de08d04f4b0674e53bde 100644 (file)
@@ -17,7 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { CodeSnippet, Link } from 'design-system';
+
+import { LinkStandalone } from '@sonarsource/echoes-react';
+import { CodeSnippet } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { translate } from '../../helpers/l10n';
@@ -30,6 +32,7 @@ interface Props {
 export default class FormattingTipsWithLink extends React.PureComponent<Props> {
   handleClick(evt: React.SyntheticEvent<HTMLAnchorElement>) {
     evt.preventDefault();
+
     window.open(
       getFormattingHelpUrl(),
       'Formatting',
@@ -40,9 +43,10 @@ export default class FormattingTipsWithLink extends React.PureComponent<Props> {
   render() {
     return (
       <div className={this.props.className}>
-        <Link onClick={this.handleClick} to="#">
+        <LinkStandalone onClick={this.handleClick} to="#">
           {translate('formatting.helplink')}
-        </Link>
+        </LinkStandalone>
+
         <p className="sw-mt-2">
           <FormattedMessage
             id="formatting.example.link"
@@ -50,6 +54,7 @@ export default class FormattingTipsWithLink extends React.PureComponent<Props> {
               example: (
                 <>
                   <br />
+
                   <CodeSnippet
                     isOneLine
                     noCopy
index 018c00e66fd6def0c26819bcafd0ca9737701081..56f6dbade96754a30df866d452b1c1171d09198b 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import * as Echoes from '@sonarsource/echoes-react';
 import * as React from 'react';
 import { Link as ReactRouterDomLink, LinkProps as ReactRouterDomLinkProps } from 'react-router-dom';
 import { isWebUri } from 'valid-url';
@@ -25,6 +26,17 @@ import DetachIcon from '../icons/DetachIcon';
 
 type OriginalLinkProps = ReactRouterDomLinkProps & React.RefAttributes<HTMLAnchorElement>;
 
+/** @deprecated Use {@link Echoes.LinkProps | LinkProps} from Echoes instead.
+ *
+ * Some of the props have changed or been renamed:
+ * - `blurAfterClick` is now `shouldBlurAfterClick`
+ * - ~`disabled`~ doesn't exist anymore, a disabled link is just a regular text
+ * - `forceExternal` is now `isExternal`
+ * - `icon` is now `iconLeft` and can only be used with LinkStandalone
+ * - `preventDefault` is now `shouldPreventDefault`
+ * - `showExternalIcon` is now `hasExternalIcon`
+ * - `stopPropagation` is now `shouldStopPropagation`
+ */
 export interface LinkProps extends OriginalLinkProps {
   size?: number;
 }
@@ -66,4 +78,6 @@ function Link({ children, size, ...props }: LinkProps, ref: React.ForwardedRef<H
   );
 }
 
+/** @deprecated Use either {@link Echoes.Link | Link} or {@link Echoes.LinkStandalone | LinkStandalone} from Echoes instead.
+ */
 export default React.forwardRef(Link);