]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21867 Create Image component with baseUrl
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Fri, 15 Mar 2024 17:24:42 +0000 (18:24 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 18 Mar 2024 20:02:30 +0000 (20:02 +0000)
24 files changed:
server/sonar-web/.eslintrc
server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx
server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx
server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx
server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx
server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
server/sonar-web/src/main/js/components/common/Image.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx
server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx
server/sonar-web/src/main/js/components/permissions/UserHolder.tsx
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx

index 3634bec594b8d5a056ef03509934b1a8e5a0d47e..e249b4c013b96a5ba02a35ab0234912fe5b17b7d 100644 (file)
@@ -9,6 +9,17 @@
   "rules": {
     "camelcase": "off",
     "promise/no-return-wrap": "warn",
+    "react/forbid-elements": [
+      "warn",
+      {
+        "forbid": [
+          {
+            "element": "img",
+            "message": "use <Image> from components/common instead"
+          }
+        ]
+      }
+    ],
     "react/jsx-curly-brace-presence": "warn",
     "testing-library/render-result-naming-convention": "off",
     /* Local rules, defined in ./eslint-local-rules/ */
index 4f8a19c4d402d122fdd403f8e6778cdfd73eaddc..51dd597b733f8847cc86dd580b0f630f660097f8 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,
@@ -24,7 +26,6 @@ import {
   CheckIcon,
   ClipboardButton,
   InputField,
-  Link,
   ListItem,
   Note,
   OrderedList,
@@ -33,6 +34,7 @@ import {
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { useSearchParams } from 'react-router-dom';
+import { Image } from '../../components/common/Image';
 import { whenLoggedIn } from '../../components/hoc/whenLoggedIn';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { generateSonarLintUserToken, portIsValid, sendUserToken } from '../../helpers/sonarlint';
@@ -88,10 +90,9 @@ export function SonarLintConnection({ currentUser }: Readonly<Props>) {
       {status === Status.request && (
         <>
           <Title>{translate('sonarlint-connection.request.title')}</Title>
-          <img
-            alt=""
+          <Image
+            alt="sonarlint-connection-request"
             className="sw-my-4"
-            role="presentation"
             src="/images/SonarLint-connection-request.png"
           />
           <p className="sw-my-4">
@@ -107,7 +108,7 @@ export function SonarLintConnection({ currentUser }: Readonly<Props>) {
 
       {status === Status.tokenError && (
         <>
-          <img alt="" className="sw-my-4 sw-pt-2" role="presentation" src="/images/cross.svg" />
+          <Image alt="sonarlint-token-error" className="sw-my-4 sw-pt-2" src="/images/cross.svg" />
           <Title>{translate('sonarlint-connection.token-error.title')}</Title>
           <p className="sw-my-4">{translate('sonarlint-connection.token-error.description')}</p>
           <p className="sw-mb-4">
@@ -116,9 +117,9 @@ export function SonarLintConnection({ currentUser }: Readonly<Props>) {
               defaultMessage={translate('sonarlint-connection.token-error.description2')}
               values={{
                 link: (
-                  <Link to="/account/security">
+                  <LinkStandalone to="/account/security">
                     {translate('sonarlint-connection.token-error.description2.link')}
-                  </Link>
+                  </LinkStandalone>
                 ),
               }}
             />
@@ -128,7 +129,11 @@ export function SonarLintConnection({ currentUser }: Readonly<Props>) {
 
       {status === Status.tokenCreated && newToken && (
         <>
-          <img alt="" className="sw-my-4 sw-pt-2" role="presentation" src="/images/check.svg" />
+          <Image
+            alt="sonarlint-connection-error"
+            className="sw-my-4 sw-pt-2"
+            src="/images/check.svg"
+          />
           <Title>{translate('sonarlint-connection.connection-error.title')}</Title>
           <p className="sw-my-6">
             {translate('sonarlint-connection.connection-error.description')}
@@ -160,10 +165,9 @@ export function SonarLintConnection({ currentUser }: Readonly<Props>) {
       {status === Status.tokenSent && newToken && (
         <>
           <Title>{translate('sonarlint-connection.success.title')}</Title>
-          <img
-            alt=""
+          <Image
+            alt="sonarlint-connection-success"
             className="sw-mb-4"
-            role="presentation"
             src="/images/SonarLint-connection-ok.png"
           />
           <p className="sw-my-4">
index 4e8bbb639713a340722f819737e3155d8621b35f..c94295f147ba8400eb45c565c87382d5c91c7eac 100644 (file)
@@ -20,9 +20,9 @@
 
 import { LinkStandalone } from '@sonarsource/echoes-react';
 import React from 'react';
+import { Image } from '../../../../../components/common/Image';
 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';
@@ -70,10 +70,10 @@ export default function PRLink({
         <LinkStandalone
           iconLeft={
             almKey !== '' && (
-              <img
+              <Image
                 alt={almKey}
                 height={16}
-                src={`${getBaseUrl()}/images/alm/${almKey}.svg`}
+                src={`/images/alm/${almKey}.svg`}
                 title={translateWithParameters('branches.see_the_pr_on_x', translate(almKey))}
               />
             )
index 6c14bb0f749c5e7e4bf1994445ca5b17f68654a1..05e799562df1f09e7551282ab1b4cacec7c3ef30 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 { MainAppBar, SonarQubeLogo } from 'design-system';
 import * as React from 'react';
+import { Image } from '../../../../components/common/Image';
 import { translate } from '../../../../helpers/l10n';
 import { GlobalSettingKeys } from '../../../../types/settings';
 import { AppStateContext } from '../../app-state/AppStateContext';
@@ -35,7 +37,7 @@ function LogoWithAriaText() {
   return (
     <div aria-label={title} role="img">
       {customLogoUrl ? (
-        <img alt={title} src={customLogoUrl} width={customLogoWidth} />
+        <Image alt={title} src={customLogoUrl} width={customLogoWidth} />
       ) : (
         <SonarQubeLogo />
       )}
index b47b0ebf58848b56024e9a58dcd3cddd1b2ebe3e..0256393b4502de5103c4836d45cb9aca45b9c5b5 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, ButtonSecondary, themeBorder, themeColor } from 'design-system';
 import * as React from 'react';
 import { dismissNotice } from '../../../api/users';
+import { Image } from '../../../components/common/Image';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
 import { NoticeType, isLoggedIn } from '../../../types/users';
 import { CurrentUserContextInterface } from '../current-user/CurrentUserContext';
 import withCurrentUserContext from '../current-user/withCurrentUserContext';
@@ -47,7 +48,7 @@ export function PromotionNotification(props: CurrentUserContextInterface) {
   return (
     <PromotionNotificationWrapper className="it__promotion_notification sw-z-global-popup sw-rounded-1 sw-flex sw-items-center sw-px-4">
       <div className="sw-mr-2">
-        <img alt="SonarQube + SonarLint" height={80} src={`${getBaseUrl()}/images/sq-sl.svg`} />
+        <Image alt="SonarQube + SonarLint" height={80} src="/images/sq-sl.svg" />
       </div>
       <PromotionNotificationContent className="sw-flex-1 sw-px-2 sw-py-4">
         <span className="sw-body-sm-highlight">{translate('promotion.sonarlint.title')}</span>
index 7c317553268074a067a3d1f53fceb8de4952c9a5..058bad865d5085854452919633b763a34f128734 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 { getTextColor } from 'design-system';
 import * as React from 'react';
 import { getIdentityProviders } from '../../../api/users';
 import { colors } from '../../../app/theme';
-import { getBaseUrl } from '../../../helpers/system';
+import { Image } from '../../../components/common/Image';
 import { IdentityProvider } from '../../../types/types';
 import { LoggedInUser } from '../../../types/users';
 
@@ -104,11 +105,11 @@ export default class UserExternalIdentity extends React.PureComponent<
             color: getTextColor(identityProvider.backgroundColor, colors.secondFontColor),
           }}
         >
-          <img
+          <Image
             alt={identityProvider.name}
             className="sw-mr-1"
             height="14"
-            src={getBaseUrl() + identityProvider.iconPath}
+            src={identityProvider.iconPath}
             width="14"
           />
           {user.externalIdentity}
index 07e97291e938e99040c8d848d4e36c29e0738c98..5c91aa611a74fe556523d947ec82cfa333ef613a 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.
  */
+
 /* eslint-disable react/no-unused-prop-types */
+
+import { LinkStandalone, Spinner } from '@sonarsource/echoes-react';
 import {
   ButtonSecondary,
   GreyCard,
   HelperHintIcon,
   LightPrimary,
-  Spinner,
-  StandoutLink,
   TextMuted,
   Title,
 } from 'design-system';
 import * as React from 'react';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import { Image } from '../../../components/common/Image';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
 import { getCreateProjectModeLocation } from '../../../helpers/urls';
 import { AlmKeys } from '../../../types/alm-settings';
 import { AppState } from '../../../types/appstate';
@@ -79,12 +80,10 @@ function renderAlmOption(
   const svgFileNameGrey = `${svgFileName}_grey`;
 
   const icon = (
-    <img
+    <Image
       alt="" // Should be ignored by screen readers
       className="sw-h-4 sw-w-4"
-      src={`${getBaseUrl()}/images/alm/${
-        !disabled && hasConfig ? svgFileName : svgFileNameGrey
-      }.svg`}
+      src={`/images/alm/${!disabled && hasConfig ? svgFileName : svgFileNameGrey}.svg`}
     />
   );
 
@@ -92,9 +91,11 @@ function renderAlmOption(
     <GreyCard key={alm} className="sw-col-span-4 sw-p-4 sw-flex sw-justify-between sw-items-center">
       <div className="sw-items-center sw-flex sw-py-2">
         {!disabled && hasConfig ? (
-          <StandoutLink icon={icon} to={getCreateProjectModeLocation(mode)}>
-            {translate('onboarding.create_project.import_select_method', alm)}
-          </StandoutLink>
+          <LinkStandalone iconLeft={icon} to={getCreateProjectModeLocation(mode)}>
+            <span className="sw-ml-2">
+              {translate('onboarding.create_project.import_select_method', alm)}
+            </span>
+          </LinkStandalone>
         ) : (
           <>
             {icon}
@@ -106,7 +107,7 @@ function renderAlmOption(
         )}
       </div>
 
-      <Spinner loading={loadingBindings}>
+      <Spinner isLoading={loadingBindings}>
         {!hasConfig &&
           (canAdmin ? (
             <ButtonSecondary onClick={() => props.onConfigMode(configMode)}>
@@ -167,9 +168,9 @@ export function CreateProjectModeSelection(props: CreateProjectModeSelectionProp
         <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12">
           <GreyCard className="sw-col-span-4 sw-p-4 sw-py-6 sw-flex sw-justify-between sw-items-center">
             <div>
-              <StandoutLink to={getCreateProjectModeLocation(CreateProjectModes.Manual)}>
+              <LinkStandalone to={getCreateProjectModeLocation(CreateProjectModes.Manual)}>
                 {translate('onboarding.create_project.import_select_method.manual')}
-              </StandoutLink>
+              </LinkStandalone>
             </div>
           </GreyCard>
         </div>
index a9c4957b5ea46d2a0a83afde7d7bfa7cb075ee9b..9ddd4b52803464c21ed7d3c949882cb1c81e3534 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 {
   ActionsDropdown,
   Badge,
@@ -33,8 +34,8 @@ import {
 } from 'design-system';
 import * as React from 'react';
 import { useState } from 'react';
+import { Image } from '../../../components/common/Image';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
 import { useGroupMembersCountQuery } from '../../../queries/group-memberships';
 import { Group, Provider } from '../../../types/types';
 import DeleteGroupForm from './DeleteGroupForm';
@@ -69,11 +70,11 @@ export default function ListItem(props: Readonly<ListItemProps>) {
     }
 
     return (
-      <img
+      <Image
         alt={identityProvider}
         className="sw-ml-2 sw-mr-2"
         height={16}
-        src={`${getBaseUrl()}/images/alm/${identityProvider}.svg`}
+        src={`/images/alm/${identityProvider}.svg`}
       />
     );
   };
index f845f0fd42934ff0af307e4c4320dd2612594a6d..a507e9e124183bc3292ae1fc16cbf0a54a383e87 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 { SubHeading, UnorderedList } from 'design-system';
 import * as React from 'react';
-import { getBaseUrl } from '../../../helpers/system';
+import { Image } from '../../../components/common/Image';
 import { Edition, EditionKey } from '../../../types/editions';
 
 interface Props {
@@ -32,11 +33,11 @@ export default function EditionBox({ edition }: Readonly<Props>) {
       return (
         <div>
           <SubHeading as="h2" id="data-center-edition">
-            <img
+            <Image
               alt="SonarQube logo"
               className="sw-mr-2"
               width={16}
-              src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+              src="/images/embed-doc/sq-icon.svg"
             />
             <span>Data Center Edition</span>
           </SubHeading>
@@ -56,11 +57,11 @@ export default function EditionBox({ edition }: Readonly<Props>) {
       return (
         <div>
           <SubHeading as="h2" id="enterprise-edition">
-            <img
+            <Image
               alt="SonarQube logo"
               className="sw-mr-2"
               width={16}
-              src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+              src="/images/embed-doc/sq-icon.svg"
             />
             <span>Enterprise Edition</span>
           </SubHeading>
@@ -83,11 +84,11 @@ export default function EditionBox({ edition }: Readonly<Props>) {
       return (
         <div>
           <SubHeading as="h2" id="developer-edition">
-            <img
+            <Image
               alt="SonarQube logo"
               className="sw-mr-2"
               width={16}
-              src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+              src="/images/embed-doc/sq-icon.svg"
             />
             <span>Developer Edition</span>
           </SubHeading>
@@ -98,28 +99,18 @@ export default function EditionBox({ edition }: Readonly<Props>) {
           <UnorderedList className="sw-ml-8" ticks>
             <li>
               <span>PR / MR decoration &amp; Quality Gate</span>
-              <img
-                alt="GitHub"
-                className="sw-ml-2"
-                src={`${getBaseUrl()}/images/alm/github.svg`}
-                width={16}
-              />
-              <img
-                alt="GitLab"
-                className="sw-ml-2"
-                src={`${getBaseUrl()}/images/alm/gitlab.svg`}
-                width={16}
-              />
-              <img
+              <Image alt="GitHub" className="sw-ml-2" src="/images/alm/github.svg" width={16} />
+              <Image alt="GitLab" className="sw-ml-2" src="/images/alm/gitlab.svg" width={16} />
+              <Image
                 alt="Azure DevOps"
                 className="sw-ml-2"
-                src={`${getBaseUrl()}/images/alm/azure.svg`}
+                src="/images/alm/azure.svg"
                 width={16}
               />
-              <img
+              <Image
                 alt="Bitbucket"
                 className="sw-ml-2"
-                src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
+                src="/images/alm/bitbucket.svg"
                 width={16}
               />
             </li>
index cc50083316b0a33af143de410db472a70462127c..104eaf2ec423a79b351c9c4d840fb0669ea3fcdf 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, Note, getTabPanelId } from 'design-system';
+
+import { Link } from '@sonarsource/echoes-react';
+import { Note, getTabPanelId } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import DocumentationLink from '../../../components/common/DocumentationLink';
+import { Image } from '../../../components/common/Image';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
 import { CodeScope, queryToSearch } from '../../../helpers/urls';
 import { Branch } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
@@ -42,7 +44,10 @@ export default function MeasuresPanelNoNewCode(props: MeasuresPanelNoNewCodeProp
   const isApp = component.qualifier === ComponentQualifier.Application;
 
   const hasBadReferenceBranch =
-    !isApp && !!period && !period.date && period.mode === NewCodeDefinitionType.ReferenceBranch;
+    !isApp &&
+    !!period &&
+    period.date === '' &&
+    period.mode === NewCodeDefinitionType.ReferenceBranch;
   /*
    * If the period is "reference branch"-based, and if there's no date, it means
    * that we're not lacking a second analysis, but that we'll never have new code because the
@@ -65,11 +70,11 @@ export default function MeasuresPanelNoNewCode(props: MeasuresPanelNoNewCodeProp
       id={getTabPanelId(CodeScope.New)}
       style={{ height: 500 }}
     >
-      <img
+      <Image
         alt="" /* Make screen readers ignore this image; it's purely eye candy. */
         className="sw-mr-2"
         height={52}
-        src={`${getBaseUrl()}/images/source-code.svg`}
+        src="/images/source-code.svg"
       />
       <Note as="div" className="sw-ml-4 sw-max-w-abs-500">
         <p className="sw-mb-2 sw-mt-4">{translate(badExplanationKey)}</p>
index 622eb382e046bd943eaaf2e841e38002a6940057..08a16d3767916b0e5306ff1f137be5acdb1e946c 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 { ButtonPrimary, FlagMessage, Title } from 'design-system';
 import * as React from 'react';
 import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
+import { Image } from '../../../../components/common/Image';
 import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
+import { isDefined } from '../../../../helpers/types';
 import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
 import { isApplication, isPortfolioLike, isProject } from '../../../../types/component';
 import { Component } from '../../../../types/types';
@@ -69,19 +71,19 @@ export default function PageHeader(props: Props) {
         <Title>
           {translate('permissions.page')}
           {provisionedByGitHub && (
-            <img
+            <Image
               alt="github"
               className="sw-mx-2 sw-align-baseline"
               aria-label={translate('project_permission.github_managed')}
               height={16}
-              src={`${getBaseUrl()}/images/alm/github.svg`}
+              src="/images/alm/github.svg"
             />
           )}
         </Title>
 
         <div>
           <p>{description}</p>
-          {visibilityDescription && <p>{visibilityDescription}</p>}
+          {isDefined(visibilityDescription) && <p>{visibilityDescription}</p>}
           {provisionedByGitHub && (
             <>
               <p>{translate('roles.page.description.github')}</p>
index 57ca2e0521e4b08bfaf476f78dcd69dffdb57105..d3ae393055612a42888d4595f9674cd84ac6023a 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 { Spinner } from '@sonarsource/echoes-react';
 import {
   BasicSeparator,
@@ -32,6 +33,7 @@ import {
 import { isEmpty } from 'lodash';
 import * as React from 'react';
 import { useState } from 'react';
+import { Image } from '../../../components/common/Image';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { localizeMetric } from '../../../helpers/measures';
@@ -103,7 +105,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) {
             onClick={() => handleSelectType(BadgeType.measure)}
             selected={BadgeType.measure === selectedType}
             image={
-              <img
+              <Image
                 alt={translate('overview.badges', BadgeType.measure, 'alt')}
                 src={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
               />
@@ -115,7 +117,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) {
             onClick={() => handleSelectType(BadgeType.qualityGate)}
             selected={BadgeType.qualityGate === selectedType}
             image={
-              <img
+              <Image
                 alt={translate('overview.badges', BadgeType.qualityGate, 'alt')}
                 src={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
                 width="128px"
index 5d7c9e57d97d727b658ac6278231451e8a54ab48..85e4547245385d905d051f8ca5a5da575f0aa1f7 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 { ItemLink } from 'design-system';
 import * as React from 'react';
+import { Image } from '../../../components/common/Image';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
 import { queryToSearch } from '../../../helpers/urls';
 import { AlmKeys } from '../../../types/alm-settings';
 
@@ -40,12 +41,7 @@ export default function ProjectCreationMenuItem(props: ProjectCreationMenuItemPr
       to={{ pathname: '/projects/create', search: queryToSearch({ mode: alm }) }}
     >
       {alm !== 'manual' && (
-        <img
-          alt={alm}
-          className="sw-mr-2"
-          width={16}
-          src={`${getBaseUrl()}/images/alm/${almIcon}.svg`}
-        />
+        <Image alt={alm} className="sw-mr-2" width={16} src={`/images/alm/${almIcon}.svg`} />
       )}
       {translate('my_account.add_project', alm)}
     </ItemLink>
index 6e21825714a1cad62de33a62bb4daeafc59966ea..8a989fae4b1e5ca560c36e6d34fa97d2065a6cf8 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 { Note } from 'design-system';
 import * as React from 'react';
 import DocumentationLink from '../../../components/common/DocumentationLink';
+import { Image } from '../../../components/common/Image';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
 
 export interface EmptyHotspotsPageProps {
   filtered: boolean;
@@ -45,13 +46,11 @@ export default function EmptyHotspotsPage(props: EmptyHotspotsPageProps) {
 
   return (
     <div className="sw-items-center sw-justify-center sw-flex-col sw-flex sw-pt-16">
-      <img
+      <Image
         alt={translate('hotspots.page')}
         className="sw-mt-8"
         height={100}
-        src={`${getBaseUrl()}/images/${
-          filtered && !filterByFile ? 'filter-large' : 'hotspot-large'
-        }.svg`}
+        src={`/images/${filtered && !filterByFile ? 'filter-large' : 'hotspot-large'}.svg`}
       />
       <h1 className="sw-mt-10 sw-body-sm-highlight">
         {translate(`hotspots.${translationRoot}.title`)}
index 611756f59ce0ba0b15b2271fca88d2ed07c4c164..ab22b7476089345d33698a1c370ba69b06e21b80 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 { Spinner } from '@sonarsource/echoes-react';
 import {
   Card,
   FlagMessage,
   PageContentFontWrapper,
-  Spinner,
   Title,
   themeBorder,
   themeColor,
 } from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
+import { Image } from '../../../components/common/Image';
 import { Location } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
 import { sanitizeUserInput } from '../../../helpers/sanitize';
-import { getBaseUrl } from '../../../helpers/system';
 import { getReturnUrl } from '../../../helpers/urls';
 import { IdentityProvider } from '../../../types/types';
 import LoginForm from './LoginForm';
@@ -54,17 +55,12 @@ export default function Login(props: Readonly<LoginProps>) {
   return (
     <div className="sw-flex sw-flex-col sw-items-center" id="login_form">
       <Helmet defer={false} title={translate('login.page')} />
-      <img alt="" className="sw-mt-32" src={`${getBaseUrl()}/images/sonar-logo-horizontal.png`} />
+      <Image alt="" className="sw-mt-32" src="/images/sonar-logo-horizontal.png" />
       <Card className="sw-my-14 sw-p-0 sw-w-abs-350">
         <PageContentFontWrapper className="sw-body-md sw-flex sw-flex-col sw-items-center sw-py-8 sw-px-4">
-          <img
-            alt=""
-            className="sw-mb-6"
-            src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
-            width={28}
-          />
+          <Image alt="" className="sw-mb-6" src="/images/embed-doc/sq-icon.svg" width={28} />
           <Title className="sw-mb-6">{translate('login.login_to_sonarqube')}</Title>
-          <Spinner loading={loading}>
+          <Spinner isLoading={loading}>
             <>
               {displayError && (
                 <FlagMessage className="sw-mb-6" variant="error">
index a0b21bed4f459796df00948d3515323aee902842..73da55bf78f9ad476ae98c22e72c146abc55fd7a 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 { FlagMessage, Link, SubTitle, ToggleButton } from 'design-system';
+
+import { Link } from '@sonarsource/echoes-react';
+import { FlagMessage, SubTitle, ToggleButton } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
+import { Image } from '../../../../components/common/Image';
 import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
+import { isDefined } from '../../../../helpers/types';
 import { useGetValuesQuery } from '../../../../queries/settings';
 import {
   AlmKeys,
@@ -56,12 +59,7 @@ const tabs = [
   {
     label: (
       <>
-        <img
-          alt="github"
-          className="sw-mr-2"
-          height={16}
-          src={`${getBaseUrl()}/images/alm/github.svg`}
-        />
+        <Image alt="github" className="sw-mr-2" height={16} src="/images/alm/github.svg" />
         {translate('settings.almintegration.tab.github')}
       </>
     ),
@@ -70,12 +68,7 @@ const tabs = [
   {
     label: (
       <>
-        <img
-          alt="bitbucket"
-          className="sw-mr-2"
-          height={16}
-          src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
-        />
+        <Image alt="bitbucket" className="sw-mr-2" height={16} src="/images/alm/bitbucket.svg" />
         {translate('settings.almintegration.tab.bitbucket')}
       </>
     ),
@@ -84,12 +77,7 @@ const tabs = [
   {
     label: (
       <>
-        <img
-          alt="azure"
-          className="sw-mr-2"
-          height={16}
-          src={`${getBaseUrl()}/images/alm/azure.svg`}
-        />
+        <Image alt="azure" className="sw-mr-2" height={16} src="/images/alm/azure.svg" />
         {translate('settings.almintegration.tab.azure')}
       </>
     ),
@@ -98,12 +86,7 @@ const tabs = [
   {
     label: (
       <>
-        <img
-          alt="gitlab"
-          className="sw-mr-2"
-          height={16}
-          src={`${getBaseUrl()}/images/alm/gitlab.svg`}
-        />
+        <Image alt="gitlab" className="sw-mr-2" height={16} src="/images/alm/gitlab.svg" />
         {translate('settings.almintegration.tab.gitlab')}
       </>
     ),
@@ -182,7 +165,7 @@ export default function AlmIntegrationRenderer(props: AlmIntegrationRendererProp
         onUpdateDefinitions={props.onUpdateDefinitions}
       />
 
-      {definitionKeyForDeletion && (
+      {isDefined(definitionKeyForDeletion) && (
         <DeleteModal
           id={definitionKeyForDeletion}
           onCancel={props.onCancelDelete}
index a3eac561c3051a3bf72e53be835697e1375031b5..5358118d64a29205ebdf73b04660e866f5dc6b2e 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 '@sonarsource/echoes-react';
 import classNames from 'classnames';
-import { FlagMessage, Link, SubTitle, ToggleButton, getTabId, getTabPanelId } from 'design-system';
+import { FlagMessage, SubTitle, ToggleButton, getTabId, getTabPanelId } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { useSearchParams } from 'react-router-dom';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from '../../../../app/components/available-features/withAvailableFeatures';
+import { Image } from '../../../../components/common/Image';
 import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
 import { searchParamsToQuery } from '../../../../helpers/urls';
 import { AlmKeys } from '../../../../types/alm-settings';
 import { Feature } from '../../../../types/features';
@@ -54,9 +56,7 @@ export const DOCUMENTATION_LINK_SUFFIXES = {
 };
 
 function renderDevOpsIcon(key: string) {
-  return (
-    <img alt={key} className="sw-mr-2" height={16} src={`${getBaseUrl()}/images/alm/${key}.svg`} />
-  );
+  return <Image alt={key} className="sw-mr-2" height={16} src={`/images/alm/${key}.svg`} />;
 }
 
 export function Authentication(props: Props & WithAvailableFeaturesProps) {
index ad93135f5aa997f6b118cce661dd3d7245e67d92..ce64e3bae5508fbdd9a730f75281e2835062284b 100644 (file)
@@ -21,8 +21,9 @@
 import { Badge, Note, getTextColor } from 'design-system';
 import * as React from 'react';
 import { colors } from '../../../app/theme';
+import { Image } from '../../../components/common/Image';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
+import { isDefined } from '../../../helpers/types';
 import { IdentityProvider, Provider } from '../../../types/types';
 import { RestUserDetailed } from '../../../types/users';
 
@@ -39,7 +40,9 @@ export default function UserListItemIdentity({ identityProvider, user, managePro
         <strong className="it__user-name sw-body-sm-highlight">{user.name}</strong>
         <Note className="it__user-login">{user.login}</Note>
       </div>
-      {user.email && <div className="it__user-email sw-mt-1">{user.email}</div>}
+      {isDefined(user.email) && user.email !== '' && (
+        <div className="it__user-email sw-mt-1">{user.email}</div>
+      )}
       {!user.local && user.externalProvider !== 'sonarqube' && (
         <ExternalProvider identityProvider={identityProvider} user={user} />
       )}
@@ -68,11 +71,11 @@ export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manage
           color: getTextColor(identityProvider.backgroundColor, colors.secondFontColor),
         }}
       >
-        <img
+        <Image
           alt={identityProvider.name}
           className="sw-mr-1"
           height="14"
-          src={getBaseUrl() + identityProvider.iconPath}
+          src={identityProvider.iconPath}
           width="14"
         />
         {user.externalLogin}
diff --git a/server/sonar-web/src/main/js/components/common/Image.tsx b/server/sonar-web/src/main/js/components/common/Image.tsx
new file mode 100644 (file)
index 0000000..e15f8df
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { getBaseUrl } from '../../helpers/system';
+
+export function Image(props: Readonly<JSX.IntrinsicElements['img']>) {
+  const { alt, src: source, ...rest } = props;
+
+  const baseUrl = getBaseUrl();
+
+  let src = source;
+
+  if (
+    src !== undefined &&
+    !src.startsWith(baseUrl) &&
+    !src.startsWith('http') &&
+    !src.startsWith('data:')
+  ) {
+    src = `${baseUrl}/${src}`.replace(/(?<!:)\/+/g, '/');
+  }
+
+  // eslint-disable-next-line react/forbid-elements
+  return <img alt={alt} src={src} {...rest} />;
+}
index 3555367e6b608abfcbbd5a61ca625ce0d7544276..8f613d698e52402713981d80b6f65715e98f92ba 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 { ItemDivider, ItemHeader, ItemLink, OpenNewTabIcon } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
-import { getBaseUrl } from '../../helpers/system';
 import { SuggestionLink } from '../../types/types';
+import { Image } from '../common/Image';
 import { DocItemLink } from './DocItemLink';
 import { SuggestionsContext } from './SuggestionsContext';
 
@@ -36,12 +37,12 @@ function IconLink({
 }) {
   return (
     <ItemLink to={link}>
-      <img
+      <Image
         alt={text}
         aria-hidden
         className="sw-mr-2"
         height="18"
-        src={`${getBaseUrl()}/images/${icon}`}
+        src={`/images/${icon}`}
         width="18"
       />
       {text}
index a1a7d38d5ad8926f4a8c19902eb552a9203cc675..fb2d9b3e3821e409e1e134454423d4f0564d8e28 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, ContentCell, TableRowInteractive, UserGroupIcon } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
-import { getBaseUrl } from '../../helpers/system';
+import { isDefined } from '../../helpers/types';
 import { Permissions } from '../../types/permissions';
 import { PermissionDefinitions, PermissionGroup } from '../../types/types';
+import { Image } from '../common/Image';
 import PermissionCell from './PermissionCell';
 import usePermissionChange from './usePermissionChange';
 
@@ -63,12 +65,12 @@ export default function GroupHolder(props: Props) {
                 <strong>{group.name}</strong>
               </div>
               {disabled && (
-                <img
+                <Image
                   alt="github"
                   className="sw-ml-2"
                   aria-label={translate('project_permission.github_managed')}
                   height={16}
-                  src={`${getBaseUrl()}/images/alm/github.svg`}
+                  src="/images/alm/github.svg"
                 />
               )}
               {group.name === ANYONE && (
@@ -77,7 +79,9 @@ export default function GroupHolder(props: Props) {
                 </Badge>
               )}
             </div>
-            {description && <div className="sw-mt-2 sw-whitespace-normal">{description}</div>}
+            {isDefined(description) && (
+              <div className="sw-mt-2 sw-whitespace-normal">{description}</div>
+            )}
           </div>
         </div>
       </ContentCell>
index e07c01b6f872cc57b8abc6dc28a7e011d09f2660..de523fdd992741f65e0323391161fe1f343caceb 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 { Avatar, ContentCell, Note, TableRowInteractive } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
-import { getBaseUrl } from '../../helpers/system';
+import { isDefined } from '../../helpers/types';
 import { PermissionDefinitions, PermissionUser } from '../../types/types';
+import { Image } from '../common/Image';
 import PermissionCell from './PermissionCell';
 import usePermissionChange from './usePermissionChange';
 
@@ -90,16 +92,16 @@ export default function UserHolder(props: Props) {
                 <Note className="sw-ml-2">{user.login}</Note>
               </div>
               {disabled && (
-                <img
+                <Image
                   alt="github"
                   className="sw-ml-2"
                   height={16}
                   aria-label={translate('project_permission.github_managed')}
-                  src={`${getBaseUrl()}/images/alm/github.svg`}
+                  src="/images/alm/github.svg"
                 />
               )}
             </div>
-            {user.email && (
+            {isDefined(user.email) && (
               <div className="sw-mt-2 sw-max-w-100 sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden">
                 {user.email}
               </div>
index 35dd791d8f4a2d18be7b66809c4c9d3eb9c2debb..9a5328edeaabbd8f9b67b779fb43b3e0a9daa86a 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { LinkHighlight, LinkStandalone, Spinner } from '@sonarsource/echoes-react';
 import {
   Breadcrumbs,
   FlagMessage,
   GreyCard,
-  HoverLink,
   LightLabel,
   LightPrimary,
-  Spinner,
-  StandoutLink,
   SubTitle,
   Title,
 } from 'design-system';
@@ -34,13 +32,13 @@ import * as React from 'react';
 import { AnalysisStatus } from '../../apps/overview/components/AnalysisStatus';
 import { isMainBranch } from '../../helpers/branch-like';
 import { translate } from '../../helpers/l10n';
-import { getBaseUrl } from '../../helpers/system';
 import { getProjectTutorialLocation } from '../../helpers/urls';
 import { useBranchesQuery } from '../../queries/branch';
 import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
 import { MainBranch } from '../../types/branch-like';
 import { Component } from '../../types/types';
 import { LoggedInUser } from '../../types/users';
+import { Image } from '../common/Image';
 import AzurePipelinesTutorial from './azure-pipelines/AzurePipelinesTutorial';
 import BitbucketPipelinesTutorial from './bitbucket-pipelines/BitbucketPipelinesTutorial';
 import GitHubActionTutorial from './github-action/GitHubActionTutorial';
@@ -66,9 +64,11 @@ export interface TutorialSelectionRendererProps {
 function renderAlm(mode: TutorialModes, project: string, icon?: React.ReactNode) {
   return (
     <GreyCard className="sw-col-span-4 sw-p-4">
-      <StandoutLink icon={icon} to={getProjectTutorialLocation(project, mode)}>
-        {translate('onboarding.tutorial.choose_method', mode)}
-      </StandoutLink>
+      <LinkStandalone iconLeft={icon} to={getProjectTutorialLocation(project, mode)}>
+        <span className={icon ? 'sw-ml-2' : ''}>
+          {translate('onboarding.tutorial.choose_method', mode)}
+        </span>
+      </LinkStandalone>
 
       {mode === TutorialModes.Local && (
         <LightLabel as="p" className="sw-mt-3">
@@ -157,10 +157,10 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
               renderAlm(
                 TutorialModes.Jenkins,
                 component.key,
-                <img
+                <Image
                   alt="" // Should be ignored by screen readers
                   className="sw-h-4 sw-w-4"
-                  src={`${getBaseUrl()}/images/tutorials/jenkins.svg`}
+                  src="/images/tutorials/jenkins.svg"
                 />,
               )}
 
@@ -168,10 +168,10 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
               renderAlm(
                 TutorialModes.GitHubActions,
                 component.key,
-                <img
+                <Image
                   alt="" // Should be ignored by screen readers
                   className="sw-h-4 sw-w-4"
-                  src={`${getBaseUrl()}/images/tutorials/github-actions.svg`}
+                  src="/images/tutorials/github-actions.svg"
                 />,
               )}
 
@@ -179,10 +179,10 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
               renderAlm(
                 TutorialModes.BitbucketPipelines,
                 component.key,
-                <img
+                <Image
                   alt="" // Should be ignored by screen readers
                   className="sw-h-4 sw-w-4"
-                  src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
+                  src="/images/alm/bitbucket.svg"
                 />,
               )}
 
@@ -190,10 +190,10 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
               renderAlm(
                 TutorialModes.GitLabCI,
                 component.key,
-                <img
+                <Image
                   alt="" // Should be ignored by screen readers
                   className="sw-h-4 sw-w-4"
-                  src={`${getBaseUrl()}/images/alm/gitlab.svg`}
+                  src="/images/alm/gitlab.svg"
                 />,
               )}
 
@@ -201,10 +201,10 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
               renderAlm(
                 TutorialModes.AzurePipelines,
                 component.key,
-                <img
+                <Image
                   alt="" // Should be ignored by screen readers
                   className="sw-h-4 sw-w-4"
-                  src={`${getBaseUrl()}/images/tutorials/azure-pipelines.svg`}
+                  src="/images/tutorials/azure-pipelines.svg"
                 />,
               )}
 
@@ -217,13 +217,19 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
 
       {selectedTutorial && (
         <Breadcrumbs className="sw-mb-3">
-          <HoverLink to={getProjectTutorialLocation(component.key)}>
+          <LinkStandalone
+            highlight={LinkHighlight.CurrentColor}
+            to={getProjectTutorialLocation(component.key)}
+          >
             {translate('onboarding.tutorial.breadcrumbs.home')}
-          </HoverLink>
+          </LinkStandalone>
 
-          <HoverLink to={getProjectTutorialLocation(component.key, selectedTutorial)}>
+          <LinkStandalone
+            highlight={LinkHighlight.CurrentColor}
+            to={getProjectTutorialLocation(component.key, selectedTutorial)}
+          >
             {translate('onboarding.tutorial.breadcrumbs', selectedTutorial)}
-          </HoverLink>
+          </LinkStandalone>
         </Breadcrumbs>
       )}
 
index 24c5cf7280279648d42e417f8aa4144bd30d88c6..a22c87b5e666b878c4fec817941df7c2f461c405 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 { LinkStandalone } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
-import { Card, LightLabel, StandoutLink } from 'design-system';
+import { Card, LightLabel } from 'design-system';
 import React from 'react';
 import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
+import { Image } from '../../common/Image';
 import { OSs, TutorialModes } from '../types';
 
 export interface GithubCFamilyExampleRepositoriesProps {
@@ -58,15 +60,15 @@ export default function GithubCFamilyExampleRepositories(
   return (
     <Card className={classNames('sw-p-4 sw-bg-inherit', className)}>
       <div>
-        <img
+        <Image
           alt="" // Should be ignored by screen readers
           className="sw-mr-2"
           height={20}
-          src={`${getBaseUrl()}/images/alm/github.svg`}
+          src="/images/alm/github.svg"
         />
-        <StandoutLink target="_blank" to={link}>
+        <LinkStandalone target="_blank" to={link}>
           sonarsource-cfamily-examples
-        </StandoutLink>
+        </LinkStandalone>
       </div>
       <LightLabel as="p" className="sw-mt-4">
         {translate('onboarding.tutorial.cfamily.examples_repositories_description')}