From 29168ab9565bcc29a752a58efb3c09848f7b2b03 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 5 Nov 2024 18:07:01 +0100 Subject: SONAR-23596 New branding SONAR-23597 SONAR-23598 SONAR-23595 --- .../src/main/js/app/components/AdminContainer.tsx | 10 +- .../main/js/app/components/ComponentContainer.tsx | 10 +- .../main/js/app/components/SonarLintConnection.tsx | 23 +-- .../components/__tests__/AdminContainer-test.tsx | 8 + .../app/components/__tests__/GlobalFooter-test.tsx | 10 +- .../indexation/IndexationNotificationRenderer.tsx | 1 - .../indexation/PageUnavailableDueToIndexation.tsx | 8 +- .../component/branch-like/BranchHelpTooltip.tsx | 13 +- .../js/app/components/nav/global/GlobalNavMore.tsx | 4 +- .../app/components/nav/global/MainSonarQubeBar.tsx | 6 +- .../PromotionNotification.tsx | 12 +- server/sonar-web/src/main/js/app/index.ts | 13 +- .../sonar-web/src/main/js/apps/account/Account.tsx | 11 +- .../components/__tests__/AuditApp-it.tsx | 8 + .../coding-rules/__tests__/CodingRuleDetails-it.ts | 11 +- .../apps/coding-rules/__tests__/CodingRules-it.ts | 8 + .../apps/coding-rules/__tests__/CustomRule-it.ts | 8 + .../coding-rules/components/CodingRulesApp.tsx | 8 +- .../js/apps/create/project/__tests__/Azure-it.tsx | 9 - .../apps/create/project/__tests__/Bitbucket-it.tsx | 9 - .../js/apps/create/project/__tests__/GitLab-it.tsx | 10 -- .../project/__tests__/MonorepoProjectCreate-it.tsx | 4 - .../main/js/apps/issues/__tests__/IssuesApp-it.tsx | 6 +- .../js/apps/issues/components/IssueDetails.tsx | 11 +- .../main/js/apps/issues/components/IssueHeader.tsx | 2 +- .../main/js/apps/issues/components/TotalEffort.tsx | 2 - .../projectNewCode/components/BranchListRow.tsx | 11 +- .../NewCodeDefinitionSettingReferenceBranch.tsx | 13 +- .../__tests__/ProjectNewCodeDefinitionApp-it.tsx | 1 + .../components/project-card/ProjectCard.tsx | 1 - .../main/js/apps/quality-gates/components/App.tsx | 10 +- .../components/ProfileContainer.tsx | 11 +- .../src/main/js/apps/sessions/components/Login.tsx | 15 +- .../main/js/apps/settings/__tests__/utils-test.ts | 8 + .../system/components/__tests__/SystemApp-it.tsx | 6 +- .../branding/SonarQubeConnectionIllustration.tsx | 199 +++++++++++++++++++++ .../branding/SonarQubeIDEPromotionIllustration.tsx | 107 +++++++++++ .../components/branding/SonarQubeProductLogo.tsx | 38 ++++ .../SonarQubeConnectionIllustration-test.tsx | 39 ++++ .../SonarQubeConnectionIllustration-test.tsx.snap | 189 +++++++++++++++++++ .../components/embed-docs-modal/EmbedDocsPopup.tsx | 53 +----- .../main/js/components/intl/TranslatedMessage.tsx | 28 +++ .../main/js/components/shared/AppVersionStatus.tsx | 5 +- .../bitbucket-pipelines/RepositoryVariables.tsx | 2 +- .../GithubCFamilyExampleRepositories.tsx | 2 +- .../tutorials/github-action/SecretStep.tsx | 2 +- .../src/main/js/components/tutorials/test-utils.ts | 8 +- .../js/components/upgrade/SystemUpgradeButton.tsx | 2 +- .../src/main/js/helpers/__tests__/l10n-test.ts | 32 +++- .../main/js/helpers/__tests__/l10nBundle-test.ts | 11 +- .../src/main/js/helpers/__tests__/measures-test.ts | 8 + server/sonar-web/src/main/js/helpers/l10n.ts | 9 +- server/sonar-web/src/main/js/helpers/l10nBundle.ts | 26 ++- .../helpers/__tests__/measures-test.ts | 13 +- 54 files changed, 879 insertions(+), 195 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/branding/SonarQubeConnectionIllustration.tsx create mode 100644 server/sonar-web/src/main/js/components/branding/SonarQubeIDEPromotionIllustration.tsx create mode 100644 server/sonar-web/src/main/js/components/branding/SonarQubeProductLogo.tsx create mode 100644 server/sonar-web/src/main/js/components/branding/__tests__/SonarQubeConnectionIllustration-test.tsx create mode 100644 server/sonar-web/src/main/js/components/branding/__tests__/__snapshots__/SonarQubeConnectionIllustration-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/intl/TranslatedMessage.tsx (limited to 'server/sonar-web/src/main/js') diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx index faa3e3c4fa3..65e122d5618 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -26,7 +26,8 @@ import { getSettingsNavigation } from '../../api/navigation'; import { getPendingPlugins } from '../../api/plugins'; import { getSystemStatus, waitSystemUPStatus } from '../../api/system'; import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; -import { translate, translateWithParameters } from '../../helpers/l10n'; +import { translate } from '../../helpers/l10n'; +import { getIntl } from '../../helpers/l10nBundle'; import { AdminPagesContext } from '../../types/admin'; import { AppState } from '../../types/appstate'; import { PendingPluginResult } from '../../types/plugins'; @@ -46,6 +47,7 @@ interface State { } export class AdminContainer extends React.PureComponent { + intl = getIntl(); mounted = false; portalAnchor: Element | null = null; state: State = { @@ -129,9 +131,9 @@ export class AdminContainer extends React.PureComponent {this.portalAnchor && diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index 34913e58aff..31fb9b35a42 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -22,6 +22,7 @@ import { differenceBy } from 'lodash'; import * as React from 'react'; import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; +import { useIntl } from 'react-intl'; import { Outlet } from 'react-router-dom'; import { CenteredLayout, Spinner } from '~design-system'; import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter'; @@ -31,7 +32,6 @@ import { validateProjectAlmBinding } from '../../api/alm-settings'; import { getTasksForComponent } from '../../api/ce'; import { getComponentData } from '../../api/components'; import { getComponentNavigation } from '../../api/navigation'; -import { translateWithParameters } from '../../helpers/l10n'; import { HttpStatus } from '../../helpers/request'; import { getPortfolioUrl, getProjectUrl, getPullRequestUrl } from '../../helpers/urls'; import { useCurrentBranchQuery } from '../../queries/branch'; @@ -63,6 +63,8 @@ function ComponentContainer({ hasFeature }: Readonly } = useLocation(); const router = useRouter(); + const intl = useIntl(); + const [component, setComponent] = React.useState(); const [currentTask, setCurrentTask] = React.useState(); const [tasksInProgress, setTasksInProgress] = React.useState(); @@ -285,9 +287,9 @@ function ComponentContainer({ hasFeature }: Readonly
{component && diff --git a/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx b/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx index cce2c4419d6..106ca9b1439 100644 --- a/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx +++ b/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx @@ -33,6 +33,7 @@ import { Title, } from '~design-system'; import { Image } from '~sonar-aligned/components/common/Image'; +import { SonarQubeConnectionIllustration } from '../../components/branding/SonarQubeConnectionIllustration'; import { whenLoggedIn } from '../../components/hoc/whenLoggedIn'; import { translate, translateWithParameters } from '../../helpers/l10n'; import { generateSonarLintUserToken, portIsValid, sendUserToken } from '../../helpers/sonarlint'; @@ -88,13 +89,9 @@ export function SonarLintConnection({ currentUser }: Readonly) { {status === Status.request && ( <> {translate('sonarlint-connection.request.title')} - sonarlint-connection-request +

- {translateWithParameters('sonarlint-connection.request.description', ideName)} +

{translate('sonarlint-connection.request.description2')}

@@ -110,7 +107,7 @@ export function SonarLintConnection({ currentUser }: Readonly) { {status === Status.tokenError && ( <> - sonarlint-token-error + {translate('sonarlint-connection.token-error.title')}

{translate('sonarlint-connection.token-error.description')}

@@ -131,11 +128,7 @@ export function SonarLintConnection({ currentUser }: Readonly) { {status === Status.tokenCreated && newToken && ( <> - sonarlint-connection-error + {translate('sonarlint-connection.connection-error.title')}

{translate('sonarlint-connection.connection-error.description')} @@ -167,11 +160,7 @@ export function SonarLintConnection({ currentUser }: Readonly) { {status === Status.tokenSent && newToken && ( <> {translate('sonarlint-connection.success.title')} - sonarlint-connection-success +

{translateWithParameters('sonarlint-connection.success.description', newToken.name)}

diff --git a/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx index 54ed12766d7..8dc51f4ca7c 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx @@ -29,6 +29,14 @@ import { AdminPagesContext } from '../../../types/admin'; import { AdminContainer, AdminContainerProps } from '../AdminContainer'; import AdminContext from '../AdminContext'; +jest.mock('../../../helpers/l10nBundle', () => { + const bundle = jest.requireActual('../../../helpers/l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn() }), + }; +}); + jest.mock('../../../api/navigation', () => ({ getSettingsNavigation: jest .fn() diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx index 70e84d95c97..b9976eb3e21 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx @@ -35,15 +35,15 @@ afterEach(() => { }); it('should render the logged-in information', async () => { - renderGlobalFooter(); + renderGlobalFooter({}, { edition: EditionKey.community }); expect(ui.databaseWarningMessage.query()).not.toBeInTheDocument(); expect(ui.footerListItems.getAll()).toHaveLength(7); expect(byText('Community Edition').get()).toBeInTheDocument(); - expect(ui.versionLabel('4.2').get()).toBeInTheDocument(); - expect(await ui.ltaDocumentationLinkActive.find()).toBeInTheDocument(); + expect(await ui.versionLabel('4.2').find()).toBeInTheDocument(); + expect(ui.ltaDocumentationLinkActive.query()).not.toBeInTheDocument(); expect(ui.apiLink.get()).toBeInTheDocument(); }); @@ -85,7 +85,7 @@ it('should not render missing logged-in information', () => { }); it('should not render the logged-in information', () => { - renderGlobalFooter({ hideLoggedInInfo: true }); + renderGlobalFooter({ hideLoggedInInfo: true }, { edition: EditionKey.community }); expect(ui.databaseWarningMessage.query()).not.toBeInTheDocument(); @@ -109,7 +109,7 @@ function renderGlobalFooter( return renderComponent(, '/', { appState: mockAppState({ productionDatabase: true, - edition: EditionKey.community, + edition: EditionKey.developer, version: '4.2', ...appStateOverride, }), diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx index c1a7473aa79..be82875356e 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx @@ -156,7 +156,6 @@ function renderInProgressBanner(completedCount: number, total: number) { { componentDidUpdate() { @@ -41,17 +40,14 @@ export class PageUnavailableDueToIndexation extends React.PureComponent - {translate('indexation.page_unavailable.description')} + - {translate('learn_more')} + ), }} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx index 3f72585ddc7..062673766cf 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx @@ -19,11 +19,12 @@ */ import { Link } from '@sonarsource/echoes-react'; +import { useIntl } from 'react-intl'; import { HelperHintIcon } from '~design-system'; import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip'; import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip'; import { DocLink } from '../../../../../helpers/doc-links'; -import { translate, translateWithParameters } from '../../../../../helpers/l10n'; +import { translate } from '../../../../../helpers/l10n'; import { getApplicationAdminUrl } from '../../../../../helpers/urls'; import { useProjectBindingQuery } from '../../../../../queries/devops-integration'; import { AlmKeys } from '../../../../../types/alm-settings'; @@ -48,6 +49,8 @@ export default function BranchHelpTooltip({ const { data: projectBinding } = useProjectBindingQuery(component.key); const isGitLab = projectBinding != null && projectBinding.alm === AlmKeys.GitLab; + const intl = useIntl(); + if (isApplication) { if (!hasManyBranches && canAdminComponent) { return ( @@ -72,9 +75,11 @@ export default function BranchHelpTooltip({ - {translate('more')} + diff --git a/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx b/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx index dc200ed30e2..5a642219a17 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx @@ -18,9 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { LogoSize } from '@sonarsource/echoes-react'; import * as React from 'react'; -import { MainAppBar, SonarQubeLogo } from '~design-system'; +import { MainAppBar } from '~design-system'; import { Image } from '~sonar-aligned/components/common/Image'; +import { SonarQubeProductLogo } from '../../../../components/branding/SonarQubeProductLogo'; import { translate } from '../../../../helpers/l10n'; import { GlobalSettingKeys } from '../../../../types/settings'; import { AppStateContext } from '../../app-state/AppStateContext'; @@ -41,7 +43,7 @@ function LogoWithAriaText() { {customLogoUrl ? ( {title} ) : ( - + )}
); diff --git a/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx b/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx index 9e828b956e3..8bf0ca1a0f9 100644 --- a/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx +++ b/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx @@ -19,11 +19,11 @@ */ import styled from '@emotion/styled'; -import { Button } from '@sonarsource/echoes-react'; +import { Button, Theme, ThemeProvider } from '@sonarsource/echoes-react'; import * as React from 'react'; import { ButtonPrimary, themeBorder, themeColor } from '~design-system'; -import { Image } from '~sonar-aligned/components/common/Image'; import { dismissNotice } from '../../../api/users'; +import { SonarQubeIDEPromotionIllustration } from '../../../components/branding/SonarQubeIDEPromotionIllustration'; import { translate } from '../../../helpers/l10n'; import { NoticeType, isLoggedIn } from '../../../types/users'; import { CurrentUserContextInterface } from '../current-user/CurrentUserContext'; @@ -48,9 +48,11 @@ export function PromotionNotification(props: CurrentUserContextInterface) { return ( -
- SonarQube + SonarLint -
+ +
+ +
+
{translate('promotion.sonarlint.title')}

{translate('promotion.sonarlint.content')}

diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index 8ad4712c238..e1b81ebe273 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -52,10 +52,17 @@ async function initApplication() { }, ); - const [l10nBundle, currentUser, appState, availableFeatures] = await Promise.all([ - loadL10nBundle(), + const appState = isMainApp() + ? await getGlobalNavigation().catch((error) => { + // eslint-disable-next-line no-console + console.error(error); + return undefined; + }) + : undefined; + + const [l10nBundle, currentUser, availableFeatures] = await Promise.all([ + loadL10nBundle(appState), isMainApp() ? getCurrentUser() : undefined, - isMainApp() ? getGlobalNavigation() : undefined, isMainApp() ? getAvailableFeatures() : undefined, ]).catch((error) => { // eslint-disable-next-line no-console diff --git a/server/sonar-web/src/main/js/apps/account/Account.tsx b/server/sonar-web/src/main/js/apps/account/Account.tsx index 8f16428327c..8c7b227834c 100644 --- a/server/sonar-web/src/main/js/apps/account/Account.tsx +++ b/server/sonar-web/src/main/js/apps/account/Account.tsx @@ -21,11 +21,12 @@ import * as React from 'react'; import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; +import { useIntl } from 'react-intl'; import { Outlet } from 'react-router-dom'; import { LargeCenteredLayout, PageContentFontWrapper, TopBar } from '~design-system'; import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget'; import { useCurrentLoginUser } from '../../app/components/current-user/CurrentUserContext'; -import { translate, translateWithParameters } from '../../helpers/l10n'; +import { translate } from '../../helpers/l10n'; import Nav from './components/Nav'; import UserCard from './components/UserCard'; @@ -33,6 +34,8 @@ export default function Account() { const currentUser = useCurrentLoginUser(); const [portalAnchor, setPortalAnchor] = React.useState(null); + const intl = useIntl(); + // Set portal anchor on mount React.useEffect(() => { setPortalAnchor(document.getElementById('component-nav-portal')); @@ -61,9 +64,9 @@ export default function Account() { diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx index 889e943b41f..daf9032ca96 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx @@ -53,6 +53,14 @@ jest.mock('../../../../helpers/dates', () => { }; }); +jest.mock('../../../../helpers/l10nBundle', () => { + const bundle = jest.requireActual('../../../../helpers/l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn(({ id }) => `${id}`) }), + }; +}); + const ui = { pageTitle: byRole('heading', { name: 'audit_logs.page' }), downloadButton: byRole('link', { name: 'download_verb' }), diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRuleDetails-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRuleDetails-it.ts index 37916c840b0..d59b743fc31 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRuleDetails-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRuleDetails-it.ts @@ -36,6 +36,15 @@ import { getPageObjects, renderCodingRulesApp } from '../utils-tests'; const rulesHandler = new CodingRulesServiceMock(); const settingsHandler = new SettingsServiceMock(); + +jest.mock('../../../helpers/l10nBundle', () => { + const bundle = jest.requireActual('../../../helpers/l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn() }), + }; +}); + afterEach(() => { settingsHandler.reset(); rulesHandler.reset(); @@ -53,7 +62,7 @@ describe('rendering', () => { expect(ui.ruleCleanCodeAttribute(CleanCodeAttribute.Clear).get()).toBeInTheDocument(); // 1 In Rule details + 1 in facet expect(ui.ruleSoftwareQuality(SoftwareQuality.Maintainability).getAll()).toHaveLength(2); - expect(document.title).toEqual('page_title.template.with_category.coding_rules.page'); + expect(document.title).toEqual('coding_rule.page.Java.Awsome java rule'); expect(screen.getByText('Why')).toBeInTheDocument(); expect(screen.getByText('Because')).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index a68adfaaf8b..7e61a2f7aa9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -38,6 +38,14 @@ import { getPageObjects, renderCodingRulesApp } from '../utils-tests'; const rulesHandler = new CodingRulesServiceMock(); const settingsHandler = new SettingsServiceMock(); +jest.mock('../../../helpers/l10nBundle', () => { + const bundle = jest.requireActual('../../../helpers/l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn() }), + }; +}); + afterEach(() => { rulesHandler.reset(); settingsHandler.reset(); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts index 0deaa28759c..73cbba1cfcc 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts @@ -28,6 +28,14 @@ import { getPageObjects, renderCodingRulesApp } from '../utils-tests'; const rulesHandler = new CodingRulesServiceMock(); const settingsHandler = new SettingsServiceMock(); +jest.mock('../../../helpers/l10nBundle', () => { + const bundle = jest.requireActual('../../../helpers/l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn() }), + }; +}); + afterEach(() => { rulesHandler.reset(); settingsHandler.reset(); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx index 1625138526e..77c670e76be 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx @@ -45,6 +45,7 @@ import { DocLink } from '../../../helpers/doc-links'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { getIntl } from '../../../helpers/l10nBundle'; import { SecurityStandard } from '../../../types/security'; import { SettingsKey } from '../../../types/settings'; import { Dict, Paging, Rule, RuleActivation } from '../../../types/types'; @@ -103,6 +104,7 @@ interface State { const RULE_LIST_HEADER_HEIGHT = 68; export class CodingRulesApp extends React.PureComponent { + intl = getIntl(); mounted = false; constructor(props: Props) { @@ -574,9 +576,9 @@ export class CodingRulesApp extends React.PureComponent { ) : ( diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx index 139b9a7fbdb..2afc4120eb6 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx @@ -60,13 +60,7 @@ const ui = { }), }; -const original = window.location; - beforeAll(() => { - Object.defineProperty(window, 'location', { - configurable: true, - value: { replace: jest.fn() }, - }); almIntegrationHandler = new AlmIntegrationsServiceMock(); dopTranslationHandler = new DopTranslationServiceMock(); newCodePeriodHandler = new NewCodeDefinitionServiceMock(); @@ -78,9 +72,6 @@ beforeEach(() => { dopTranslationHandler.reset(); newCodePeriodHandler.reset(); }); -afterAll(() => { - Object.defineProperty(window, 'location', { configurable: true, value: original }); -}); it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => { const user = userEvent.setup(); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx index d3ee2e65328..2bee7a314df 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx @@ -55,13 +55,8 @@ const ui = { }), instanceSelector: byLabelText(/alm.configuration.selector.label/), }; -const original = window.location; beforeAll(() => { - Object.defineProperty(window, 'location', { - configurable: true, - value: { replace: jest.fn() }, - }); almIntegrationHandler = new AlmIntegrationsServiceMock(); dopTranslationHandler = new DopTranslationServiceMock(); newCodePeriodHandler = new NewCodeDefinitionServiceMock(); @@ -74,10 +69,6 @@ beforeEach(() => { newCodePeriodHandler.reset(); }); -afterAll(() => { - Object.defineProperty(window, 'location', { configurable: true, value: original }); -}); - it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => { const user = userEvent.setup(); renderCreateProject(); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx index ca52dfa93b4..20417f5162a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx @@ -83,13 +83,7 @@ const ui = { globalSettingRadio: byRole('radio', { name: 'new_code_definition.global_setting' }), }; -const original = window.location; - beforeAll(() => { - Object.defineProperty(window, 'location', { - configurable: true, - value: { replace: jest.fn() }, - }); almIntegrationHandler = new AlmIntegrationsServiceMock(); dopTranslationHandler = new DopTranslationServiceMock(); newCodePeriodHandler = new NewCodeDefinitionServiceMock(); @@ -102,10 +96,6 @@ beforeEach(() => { newCodePeriodHandler.reset(); }); -afterAll(() => { - Object.defineProperty(window, 'location', { configurable: true, value: original }); -}); - it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => { const user = userEvent.setup(); renderCreateProject(); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/MonorepoProjectCreate-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/MonorepoProjectCreate-it.tsx index ce4a24d6a91..a608af96767 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/MonorepoProjectCreate-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/MonorepoProjectCreate-it.tsx @@ -77,10 +77,6 @@ const ui = { }; beforeAll(() => { - Object.defineProperty(window, 'location', { - configurable: true, - value: { replace: jest.fn() }, - }); almIntegrationHandler = new AlmIntegrationsServiceMock(); almSettingsHandler = new AlmSettingsServiceMock(); componentsHandler = new ComponentsServiceMock(); diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index 051bdfb50ea..abf661e987b 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -130,7 +130,7 @@ describe('issues app', () => { // Are rule headers present? expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /^advancedRuleId/ })).toBeInTheDocument(); // Select the "why is this an issue" tab and check its content await user.click( @@ -182,7 +182,7 @@ describe('issues app', () => { // Are rule headers present? expect(screen.getByRole('heading', { level: 1, name: 'Fix this' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /^simpleRuleId/ })).toBeInTheDocument(); // Select the "why is this an issue tab" and check its content await user.click( @@ -195,7 +195,7 @@ describe('issues app', () => { // Are rule headers present? expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /^simpleRuleId/ })).toBeInTheDocument(); // The "Where is the issue" tab should be selected by default. Check its content expect(screen.getAllByRole('button', { name: 'Issue on file' })).toHaveLength(2); // there will be 2 buttons one in concise issue and other in code viewer diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx index b6f788ab3d0..0179cd64c9a 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx @@ -21,6 +21,7 @@ import styled from '@emotion/styled'; import { Spinner } from '@sonarsource/echoes-react'; import { Helmet } from 'react-helmet-async'; +import { useIntl } from 'react-intl'; import { FlagMessage, LargeCenteredLayout, @@ -33,7 +34,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe import { IssueSuggestionCodeTab } from '../../../components/rules/IssueSuggestionCodeTab'; import IssueTabViewer from '../../../components/rules/IssueTabViewer'; import { fillBranchLike } from '../../../helpers/branch-like'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { useRuleDetailsQuery } from '../../../queries/rules'; import A11ySkipTarget from '../../../sonar-aligned/components/a11y/A11ySkipTarget'; import { isPortfolioLike } from '../../../sonar-aligned/helpers/component'; @@ -82,6 +83,8 @@ export default function IssueDetails({ const { data: ruleData, isLoading: isLoadingRule } = useRuleDetailsQuery({ key: openIssue.rule }); const openRuleDetails = ruleData?.rule; + const intl = useIntl(); + const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && (

{translate('issues.page')}

diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx index 99d2cccca84..cc01e0de5f0 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx @@ -165,7 +165,7 @@ export default class IssueHeader extends React.PureComponent { {isExternal ? ( ({key}) ) : ( - + {key} )} diff --git a/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx b/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx index 155bbc8878b..4a2111cf2fe 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx @@ -20,13 +20,11 @@ import { FormattedMessage } from 'react-intl'; import { formatMeasure } from '~sonar-aligned/helpers/measures'; -import { translate } from '../../../helpers/l10n'; export default function TotalEffort({ effort }: { effort: number }) { return (
{formatMeasure(effort, 'WORK_DUR')} }} /> diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx index d14f7713ba4..c644d5e774c 100644 --- a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx @@ -26,6 +26,7 @@ import { IconMoreVertical, Tooltip, } from '@sonarsource/echoes-react'; +import { useIntl } from 'react-intl'; import { ActionCell, Badge, @@ -97,6 +98,8 @@ function referenceBranchDoesNotExist( export default function BranchListRow(props: BranchListRowProps) { const { branch, existingBranches, inheritedSetting } = props; + const intl = useIntl(); + let settingWarning: string | undefined; if (branchInheritsItselfAsReference(branch, inheritedSetting)) { settingWarning = translateWithParameters( @@ -104,9 +107,11 @@ export default function BranchListRow(props: BranchListRowProps) { branch.name, ); } else if (referenceBranchDoesNotExist(branch, existingBranches)) { - settingWarning = translateWithParameters( - 'baseline.reference_branch.does_not_exist', - branch.newCodePeriod?.value ?? '', + settingWarning = intl.formatMessage( + { + id: 'baseline.reference_branch.does_not_exist', + }, + { branch: branch.newCodePeriod?.value ?? '' }, ); } diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx index 10d64eb50c1..1b2456a3c11 100644 --- a/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx +++ b/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx @@ -18,12 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { FormattedMessage } from 'react-intl'; import { MenuPlacement, OptionProps, components } from 'react-select'; import { Badge, FlagErrorIcon, FormField, InputSelect, SelectionCard } from '~design-system'; import Tooltip from '../../../components/controls/Tooltip'; import { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { NewCodeDefinitionType } from '../../../types/new-code-definition'; export interface BaselineSettingReferenceBranchProps { @@ -60,10 +61,12 @@ function renderBranchOption(props: OptionProps) { {option.isInvalid ? ( + } > {option.value} diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx index 8758e89fc38..e2a7bc5e9a8 100644 --- a/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx @@ -394,6 +394,7 @@ function getPageObjects() { branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/), dismissButton: byLabelText('dismiss'), baselineSpecificAnalysisDate: byText(/January 10, 2018/), + missingReferenceBranchWarning: byText('baseline.reference_branch.does_not_exist'), }; async function appIsLoaded() { diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx index 9ea67fc2774..93f608d26cd 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx @@ -164,7 +164,6 @@ function renderFirstLine( , }} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx index ad5d7b1510f..0a1b3363e22 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx @@ -22,6 +22,7 @@ import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useCallback, useEffect } from 'react'; import { Helmet } from 'react-helmet-async'; +import { useIntl } from 'react-intl'; import { useNavigate, useParams } from 'react-router-dom'; import { Card, @@ -36,7 +37,7 @@ import { import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import '../../../components/search-navigator.css'; import { DocLink } from '../../../helpers/doc-links'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; import { useQualityGatesQuery } from '../../../queries/quality-gates'; import { QualityGate } from '../../../types/types'; @@ -47,6 +48,7 @@ import ListHeader from './ListHeader'; export default function App() { const { data, isLoading } = useQualityGatesQuery(); + const intl = useIntl(); const { name } = useParams(); const navigate = useNavigate(); const { @@ -79,9 +81,9 @@ export default function App() {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx index f8c72d29f8d..a47c1873b6c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx @@ -20,9 +20,10 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { useIntl } from 'react-intl'; import { Outlet, useSearchParams } from 'react-router-dom'; import { useLocation } from '~sonar-aligned/components/hoc/withRouter'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import ProfileHeader from '../details/ProfileHeader'; import { useQualityProfilesContext } from '../qualityProfilesContext'; import ProfileNotFound from './ProfileNotFound'; @@ -36,6 +37,8 @@ export default function ProfileContainer() { const context = useQualityProfilesContext(); const { profiles } = context; + const intl = useIntl(); + // try to find a quality profile with the given key // if managed to find one, redirect to a new version // otherwise show not found page @@ -63,9 +66,9 @@ export default function ProfileContainer() { ) { const displayError = Boolean(location.query.authorizationError); return ( -
+
- + - - {translate('login.login_to_sonarqube')} + + + <FormattedMessage id="login.login_to_sonarqube" /> + <> {displayError && ( diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts index 530c8a9baa3..6e53ac929b9 100644 --- a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts @@ -40,6 +40,14 @@ jest.mock('../../../helpers/l10n', () => ({ hasMessage: jest.fn(), })); +jest.mock('../../../helpers/l10nBundle', () => { + const bundle = jest.requireActual('../../../helpers/l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn(({ id }) => `${id}`) }), + }; +}); + const fields = [ { key: 'foo', type: 'STRING' } as SettingFieldDefinition, { key: 'bar', type: 'SINGLE_SELECT_LIST' } as SettingFieldDefinition, diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx index f15e9e0128d..bd30797fb90 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx @@ -23,8 +23,10 @@ import userEvent from '@testing-library/user-event'; import { first } from 'lodash'; import { byRole, byText } from '~sonar-aligned/helpers/testSelector'; import SystemServiceMock from '../../../../api/mocks/SystemServiceMock'; +import { mockAppState } from '../../../../helpers/testMocks'; import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; import { AppState } from '../../../../types/appstate'; +import { EditionKey } from '../../../../types/editions'; import routes from '../../routes'; import { LogsLevels } from '../../utils'; @@ -125,7 +127,9 @@ describe('System Info Cluster', () => { }); function renderSystemApp(appState?: AppState) { - return renderAppRoutes('system', routes, { appState }); + return renderAppRoutes('system', routes, { + appState: mockAppState({ edition: EditionKey.developer, ...appState }), + }); } function getPageObjects() { diff --git a/server/sonar-web/src/main/js/components/branding/SonarQubeConnectionIllustration.tsx b/server/sonar-web/src/main/js/components/branding/SonarQubeConnectionIllustration.tsx new file mode 100644 index 00000000000..11c7b8308be --- /dev/null +++ b/server/sonar-web/src/main/js/components/branding/SonarQubeConnectionIllustration.tsx @@ -0,0 +1,199 @@ +/* + * 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 { useAppState } from '../../app/components/app-state/withAppStateContext'; +import { EditionKey } from '../../types/editions'; + +interface Props { + className?: string; + connected: boolean; +} + +/** + * This component switches between the Community and Server product versions' logo, + * and between the connected and requested versions of the illustration + */ +export function SonarQubeConnectionIllustration(props: Props) { + const { edition } = useAppState(); + + return edition === EditionKey.community ? ( + + ) : ( + + ); +} + +function CommunityIllustration({ connected, ...imageProps }: Props) { + return connected ? ( + + + + + + + + + ) : ( + + + + + + + + + ); +} + +function ServerIllustration({ connected, ...imageProps }: Props) { + return connected ? ( + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/branding/SonarQubeIDEPromotionIllustration.tsx b/server/sonar-web/src/main/js/components/branding/SonarQubeIDEPromotionIllustration.tsx new file mode 100644 index 00000000000..3e4ea0bc9db --- /dev/null +++ b/server/sonar-web/src/main/js/components/branding/SonarQubeIDEPromotionIllustration.tsx @@ -0,0 +1,107 @@ +/* + * 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 { useAppState } from '../../app/components/app-state/withAppStateContext'; +import { EditionKey } from '../../types/editions'; + +interface Props { + className?: string; +} +/** + * This component switches between the Community and Server product versions' logo + */ +export function SonarQubeIDEPromotionIllustration({ className }: Props) { + const { edition } = useAppState(); + + return edition === EditionKey.community ? ( + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/branding/SonarQubeProductLogo.tsx b/server/sonar-web/src/main/js/components/branding/SonarQubeProductLogo.tsx new file mode 100644 index 00000000000..a9db4c2aa96 --- /dev/null +++ b/server/sonar-web/src/main/js/components/branding/SonarQubeProductLogo.tsx @@ -0,0 +1,38 @@ +/* + * 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 { LogoSonarQubeCommunity, LogoSonarQubeServer } from '@sonarsource/echoes-react'; +import * as React from 'react'; +import { useAppState } from '../../app/components/app-state/withAppStateContext'; +import { EditionKey } from '../../types/editions'; + +type Props = React.ComponentProps; + +/** + * This component switches between the Community and Server product versions' logo + */ +export function SonarQubeProductLogo(props: Props) { + const { edition } = useAppState(); + + const OfficialLogo = + edition === EditionKey.community ? LogoSonarQubeCommunity : LogoSonarQubeServer; + + return ; +} diff --git a/server/sonar-web/src/main/js/components/branding/__tests__/SonarQubeConnectionIllustration-test.tsx b/server/sonar-web/src/main/js/components/branding/__tests__/SonarQubeConnectionIllustration-test.tsx new file mode 100644 index 00000000000..b95f33683f1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/branding/__tests__/SonarQubeConnectionIllustration-test.tsx @@ -0,0 +1,39 @@ +/* + * 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 { mockAppState } from '../../../helpers/testMocks'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; +import { EditionKey } from '../../../types/editions'; +import { SonarQubeConnectionIllustration } from '../SonarQubeConnectionIllustration'; + +it.each([ + [EditionKey.community, true], + [EditionKey.community, false], + [EditionKey.enterprise, true], + [EditionKey.enterprise, false], +])('should render %s edition (variant connected %s) correctly', (edition, connected) => { + const { container } = renderComponent( + , + '', + { appState: mockAppState({ edition }) }, + ); + + expect(container).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/branding/__tests__/__snapshots__/SonarQubeConnectionIllustration-test.tsx.snap b/server/sonar-web/src/main/js/components/branding/__tests__/__snapshots__/SonarQubeConnectionIllustration-test.tsx.snap new file mode 100644 index 00000000000..400a3b1350d --- /dev/null +++ b/server/sonar-web/src/main/js/components/branding/__tests__/__snapshots__/SonarQubeConnectionIllustration-test.tsx.snap @@ -0,0 +1,189 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render community edition (variant connected false) correctly 1`] = ` +
+ +
+`; + +exports[`should render community edition (variant connected true) correctly 1`] = ` +
+ +
+`; + +exports[`should render enterprise edition (variant connected false) correctly 1`] = ` +
+ +
+`; + +exports[`should render enterprise edition (variant connected true) correctly 1`] = ` +
+ +
+`; diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx index 5bffded6ae8..9824cec5a8f 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -20,41 +20,12 @@ import { DropdownMenu } from '@sonarsource/echoes-react'; import * as React from 'react'; -import { Image } from '~sonar-aligned/components/common/Image'; import { DocLink } from '../../helpers/doc-links'; import { translate } from '../../helpers/l10n'; import { SuggestionLink } from '../../types/types'; import { DocItemLink } from './DocItemLink'; import { SuggestionsContext } from './SuggestionsContext'; -function IconLink({ - icon = 'embed-doc/sq-icon.svg', - link, - text, -}: { - icon?: string; - link: string; - text: string; -}) { - return ( - - } - to={link} - > - {text} - - ); -} - function Suggestions({ suggestions }: Readonly<{ suggestions: SuggestionLink[] }>) { return ( <> @@ -103,21 +74,15 @@ export function EmbedDocsPopup() { {translate('docs.stay_connected')} - - - - - + + {translate('docs.news')} + + + + {translate('docs.roadmap')} + + + X @SonarQube ); } diff --git a/server/sonar-web/src/main/js/components/intl/TranslatedMessage.tsx b/server/sonar-web/src/main/js/components/intl/TranslatedMessage.tsx new file mode 100644 index 00000000000..497103386cd --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/TranslatedMessage.tsx @@ -0,0 +1,28 @@ +/* + * 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 { ComponentProps } from 'react'; +import { FormattedMessage } from 'react-intl'; + +type Props = ComponentProps; + +export function TranslatedMessage(props: Props) { + return ; +} diff --git a/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx b/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx index 8b9452fe2ff..bd481c9c061 100644 --- a/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx +++ b/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx @@ -27,10 +27,11 @@ import { useDocUrl } from '../../helpers/docs'; import { getInstanceVersionNumber } from '../../helpers/strings'; import { isCurrentVersionEOLActive } from '../../helpers/system'; import { useSystemUpgrades } from '../../queries/system'; +import { EditionKey } from '../../types/editions'; export default function AppVersionStatus() { const { data } = useSystemUpgrades(); - const { version, versionEOL } = useAppState(); + const { edition, version, versionEOL } = useAppState(); const isActiveVersion = useMemo(() => { if (data?.installedVersionActive !== undefined) { @@ -47,7 +48,7 @@ export default function AppVersionStatus() { { id: `footer.version` }, { version: getInstanceVersionNumber(version), - status: ( + status: edition && edition !== EditionKey.community && ( {translate('onboarding.tutorial.with.bitbucket_pipelines.variables.intro.link')} diff --git a/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx b/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx index eaf7bdf367d..a4606e48efd 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx @@ -65,7 +65,7 @@ export default function GithubCFamilyExampleRepositories( height={20} src="/images/alm/github.svg" /> - + sonarsource-cfamily-examples
diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx index acb6dfef1db..38f0f198ee8 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx @@ -59,7 +59,7 @@ export default function SecretStep(props: SecretStepProps) { almBinding && projectBinding ? ( {translate('onboarding.tutorial.with.github_action.secret.intro.link')} diff --git a/server/sonar-web/src/main/js/components/tutorials/test-utils.ts b/server/sonar-web/src/main/js/components/tutorials/test-utils.ts index aee023dcbeb..f46255e766c 100644 --- a/server/sonar-web/src/main/js/components/tutorials/test-utils.ts +++ b/server/sonar-web/src/main/js/components/tutorials/test-utils.ts @@ -71,9 +71,11 @@ export function getCommonNodes(ci: TutorialModes) { expiresInSelect: byRole('combobox', { name: '' }), tokenValue: byText('generatedtoken2'), linkToRepo: byRole('link', { - name: `onboarding.tutorial.with.${CI_TRANSLATE_MAP[ci]}.${ - ci === TutorialModes.GitHubActions ? 'secret' : 'variables' - }.intro.link`, + name: new RegExp( + `onboarding.tutorial.with.${CI_TRANSLATE_MAP[ci]}.${ + ci === TutorialModes.GitHubActions ? 'secret' : 'variables' + }.intro.link`, + ), }), allSetSentence: byText('onboarding.tutorial.ci_outro.done'), }; diff --git a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx index f6ee51f0630..d3f89677405 100644 --- a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx @@ -50,7 +50,7 @@ export default function SystemUpgradeButton(props: Readonly) { {translate('learn_more')} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts index 39d66bb80f8..e1d3df12fce 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { IntlShape } from 'react-intl'; import { Dict } from '../../types/types'; import { getLocalizedCategoryMetricName, @@ -30,18 +31,30 @@ import { translate, translateWithParameters, } from '../l10n'; -import { getMessages } from '../l10nBundle'; +import { getIntl, getMessages } from '../l10nBundle'; const MSG = 'my_message'; jest.unmock('../l10n'); -jest.mock('../l10nBundle', () => ({ - getMessages: jest.fn().mockReturnValue({}), -})); +jest.mock('../l10nBundle', () => { + const bundle = jest.requireActual('../l10nBundle'); + return { + ...bundle, + getIntl: jest.fn().mockReturnValue({ formatMessage: jest.fn(({ id }) => `${id}`) }), + getMessages: jest.fn().mockReturnValue({}), + }; +}); + +const resetMessages = (messages: Dict) => { + jest.mocked(getMessages).mockReturnValue(messages); -const resetMessages = (messages: Dict) => - (getMessages as jest.Mock).mockReturnValue(messages); + jest.mocked(getIntl).mockReturnValue({ + formatMessage: jest.fn(({ id }) => { + return id ? (messages[id] ?? id) : `${id}`; + }), + } as unknown as IntlShape); +}; beforeEach(() => { resetMessages({}); @@ -82,6 +95,13 @@ describe('translate', () => { expect(translate('random', 'key')).toBe('random.key'); expect(translate('composite.random', 'key')).toBe('composite.random.key'); }); + + it('should fall back to the old system when intl is undefined', () => { + jest.mocked(getIntl).mockReturnValueOnce(undefined as unknown as IntlShape); + resetMessages({ exists: 'this exists' }); + + expect(translate('exists')).toBe('this exists'); + }); }); describe('translateWithParameters', () => { diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts index 5ac785375b5..930f244e8a9 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts @@ -20,6 +20,7 @@ import { fetchL10nBundle } from '../../api/l10n'; import { loadL10nBundle } from '../l10nBundle'; +import { mockAppState } from '../testMocks'; beforeEach(() => { jest.clearAllMocks(); @@ -33,9 +34,11 @@ jest.mock('../../api/l10n', () => ({ }), })); +const APP_STATE = mockAppState({}); + describe('#loadL10nBundle', () => { it('should fetch bundle without any timestamp', async () => { - await loadL10nBundle(); + await loadL10nBundle(APP_STATE); expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined }); }); @@ -44,7 +47,7 @@ describe('#loadL10nBundle', () => { const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } }; (window as unknown as any).sonarQubeL10nBundle = cachedBundle; - await loadL10nBundle(); + await loadL10nBundle(APP_STATE); expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined }); }); @@ -53,7 +56,7 @@ describe('#loadL10nBundle', () => { const cachedBundle = { timestamp: 'timestamp', locale: 'de', messages: { cache: 'cache' } }; (window as unknown as any).sonarQubeL10nBundle = cachedBundle; - await loadL10nBundle(); + await loadL10nBundle(APP_STATE); expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: cachedBundle.timestamp }); }); @@ -63,7 +66,7 @@ describe('#loadL10nBundle', () => { (fetchL10nBundle as jest.Mock).mockRejectedValueOnce({ status: 304 }); (window as unknown as any).sonarQubeL10nBundle = cachedBundle; - const bundle = await loadL10nBundle(); + const bundle = await loadL10nBundle(APP_STATE); expect(bundle).toEqual( expect.objectContaining({ locale: cachedBundle.locale, messages: cachedBundle.messages }), diff --git a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts index 09527fbb946..4c8516d50fb 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts @@ -28,6 +28,14 @@ import { import { mockQualityGateStatusCondition } from '../mocks/quality-gates'; import { mockMeasure, mockMeasureEnhanced, mockMetric } from '../testMocks'; +jest.mock('../l10nBundle', () => { + const bundle = jest.requireActual('../l10nBundle'); + return { + ...bundle, + getIntl: () => ({ formatMessage: jest.fn(({ id }) => `${id}`) }), + }; +}); + describe('enhanceConditionWithMeasure', () => { it('should correctly map enhance conditions with measure data', () => { const measures = [ diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts index 920a5a6c577..4594e4ae636 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.ts +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getMessages } from './l10nBundle'; +import { getIntl, getMessages } from './l10nBundle'; export function hasMessage(...keys: string[]): boolean { const messageKey = keys.join('.'); @@ -35,9 +35,14 @@ export function translate(...keys: string[]): string { console.error(`No message for: ${messageKey}`); } - return l10nMessages[messageKey] || messageKey; + const intl = getIntl(); + // fallback to old if in extension + return intl ? intl.formatMessage({ id: messageKey }) : l10nMessages[messageKey]; } +/** + * @param messageKey @deprecated Use react-intl instead + */ export function translateWithParameters( messageKey: string, ...parameters: Array diff --git a/server/sonar-web/src/main/js/helpers/l10nBundle.ts b/server/sonar-web/src/main/js/helpers/l10nBundle.ts index 4fc589c47f8..d34b1f479bb 100644 --- a/server/sonar-web/src/main/js/helpers/l10nBundle.ts +++ b/server/sonar-web/src/main/js/helpers/l10nBundle.ts @@ -20,9 +20,12 @@ import { IntlShape, createIntl, createIntlCache } from 'react-intl'; import { fetchL10nBundle } from '../api/l10n'; +import { AppState } from '../types/appstate'; +import { EditionKey } from '../types/editions'; import { L10nBundle, L10nBundleRequestParams } from '../types/l10nBundle'; import { Dict } from '../types/types'; import { toISO8601WithOffsetString } from './dates'; +import { isDefined } from './types'; const DEFAULT_LOCALE = 'en'; const DEFAULT_MESSAGES: Dict = { @@ -48,7 +51,7 @@ export function getCurrentL10nBundle() { return getL10nBundleFromCache(); } -export async function loadL10nBundle() { +export async function loadL10nBundle(appState: AppState | undefined) { const browserLocale = getPreferredLanguage(); const cachedBundle = getL10nBundleFromCache(); @@ -91,6 +94,17 @@ export async function loadL10nBundle() { { locale: effectiveLocale, messages, + + /* + * This sets a default value for translations, so devs do not need to pass the {productName} + * value to every instance of FormattedMessage. + * It is a bit of a hack, abusing this config item that is normally for tag replacement only, + * hence the ts-expect-error tag + */ + defaultRichTextElements: { + // @ts-expect-error + productName: getProductName(appState), + }, }, cache, ); @@ -109,3 +123,13 @@ function getL10nBundleFromCache(): L10nBundle { function persistL10nBundleInCache(bundle: L10nBundle) { (window as unknown as any).sonarQubeL10nBundle = bundle; } + +function getProductName(appState?: AppState) { + if (isDefined(appState?.edition)) { + return appState?.edition === EditionKey.community + ? 'SonarQube Community Build' + : 'SonarQube Server'; + } + + return 'SonarQube'; +} diff --git a/server/sonar-web/src/main/js/sonar-aligned/helpers/__tests__/measures-test.ts b/server/sonar-web/src/main/js/sonar-aligned/helpers/__tests__/measures-test.ts index 223898cb1d1..f5ca0dfd212 100644 --- a/server/sonar-web/src/main/js/sonar-aligned/helpers/__tests__/measures-test.ts +++ b/server/sonar-web/src/main/js/sonar-aligned/helpers/__tests__/measures-test.ts @@ -18,8 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { IntlShape } from 'react-intl'; import { MetricType } from '~sonar-aligned/types/metrics'; -import { getMessages } from '../../../helpers/l10nBundle'; +import { getIntl, getMessages } from '../../../helpers/l10nBundle'; import { Dict } from '../../../types/types'; import { formatMeasure } from '../measures'; @@ -33,11 +34,19 @@ jest.unmock('../../../helpers/l10n'); jest.mock('../../../helpers/l10nBundle', () => ({ getCurrentLocale: jest.fn().mockReturnValue('us'), getMessages: jest.fn().mockReturnValue({}), + getIntl: jest.fn().mockReturnValue({ formatMessage: jest.fn(({ id }) => `${id}`) }), })); -const resetMessages = (messages: Dict) => +const resetMessages = (messages: Dict) => { jest.mocked(getMessages).mockReturnValue(messages); + jest.mocked(getIntl).mockReturnValue({ + formatMessage: jest.fn(({ id }) => { + return id ? (messages[id] ?? id) : `${id}`; + }), + } as unknown as IntlShape); +}; + beforeAll(() => { resetMessages({ 'work_duration.x_days': '{0}d', -- cgit v1.2.3