From: Philippe Perrin Date: Fri, 15 May 2020 07:15:38 +0000 (+0200) Subject: Bump to sonar-ui-common@1.0.0 X-Git-Tag: 8.4.0.35506~130 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=fd91b02d132511a2436722d77161f746d01910cd;p=sonarqube.git Bump to sonar-ui-common@1.0.0 --- diff --git a/server/sonar-docs/package.json b/server/sonar-docs/package.json index 112755dae67..cfdcbc2fa75 100644 --- a/server/sonar-docs/package.json +++ b/server/sonar-docs/package.json @@ -21,7 +21,7 @@ "react-dom": "16.13.0", "react-helmet": "5.2.1", "react-typography": "0.16.19", - "sonar-ui-common": "0.0.58", + "sonar-ui-common": "1.0.0", "typography": "0.16.19" }, "devDependencies": { diff --git a/server/sonar-docs/yarn.lock b/server/sonar-docs/yarn.lock index 9eb9c218575..66034d35704 100644 --- a/server/sonar-docs/yarn.lock +++ b/server/sonar-docs/yarn.lock @@ -12736,10 +12736,10 @@ sockjs@0.3.19: faye-websocket "^0.10.0" uuid "^3.0.1" -sonar-ui-common@0.0.58: - version "0.0.58" - resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-0.0.58.tgz#860440bd476d176c71828e9b82e193384cd57f66" - integrity sha1-hgRAvUdtF2xxgo6bguGTOEzVf2Y= +sonar-ui-common@1.0.0: + version "1.0.0" + resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-1.0.0.tgz#060bce001925fcce1b86696058819941d3883c63" + integrity sha1-BgvOABkl/M4bhmlgWIGZQdOIPGM= dependencies: "@types/react-select" "1.2.6" classnames "2.2.6" diff --git a/server/sonar-web/config/jest/SetupTestEnvironment.js b/server/sonar-web/config/jest/SetupTestEnvironment.js deleted file mode 100644 index bfd615891b6..00000000000 --- a/server/sonar-web/config/jest/SetupTestEnvironment.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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. - */ -window.baseUrl = ''; -window.t = window.tp = function() { - const args = Array.prototype.slice.call(arguments, 0); - return args.join('.'); -}; - -const content = document.createElement('div'); -content.id = 'content'; -document.documentElement.appendChild(content); diff --git a/server/sonar-web/config/jest/SetupTestEnvironment.ts b/server/sonar-web/config/jest/SetupTestEnvironment.ts new file mode 100644 index 00000000000..e60b3048b4c --- /dev/null +++ b/server/sonar-web/config/jest/SetupTestEnvironment.ts @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 SonarUiCommonInitializer, { DEFAULT_LOCALE } from 'sonar-ui-common/helpers/init'; + +const content = document.createElement('div'); +content.id = 'content'; +document.documentElement.appendChild(content); + +const baseUrl = ''; +(window as any).baseUrl = baseUrl; +SonarUiCommonInitializer.setLocale(DEFAULT_LOCALE) + .setMessages({}) + .setUrlContext(baseUrl); diff --git a/server/sonar-web/jest.config.js b/server/sonar-web/jest.config.js new file mode 100644 index 00000000000..37a3d45c5ad --- /dev/null +++ b/server/sonar-web/jest.config.js @@ -0,0 +1,32 @@ +module.exports = { + coverageDirectory: '/coverage', + collectCoverageFrom: ['src/main/js/**/*.{ts,tsx,js}'], + coverageReporters: ['lcovonly', 'text'], + globals: { + 'ts-jest': { + diagnostics: { + ignoreCodes: [151001] + } + } + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + moduleNameMapper: { + '^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/config/jest/FileStub.js', + '^.+\\.css$': '/config/jest/CSSStub.js', + '^Docs/@types/types$': '/../sonar-docs/src/@types/types.d.ts', + '^Docs/(.*)': '/../sonar-docs/src/$1' + }, + setupFiles: [ + '/config/polyfills.js', + '/config/jest/SetupEnzyme.js', + '/config/jest/SetupTestEnvironment.ts' + ], + snapshotSerializers: ['enzyme-to-json/serializer'], + testPathIgnorePatterns: ['/config', '/node_modules', '/scripts'], + testRegex: '(/__tests__/.*|\\-test)\\.(ts|tsx|js)$', + transform: { + '\\.js$': 'babel-jest', + '\\.(ts|tsx)$': 'ts-jest' + } +}; diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index f7b0ff36cf8..43696825ec1 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -38,7 +38,7 @@ "rehype-slug": "3.0.0", "remark-custom-blocks": "2.5.0", "remark-rehype": "6.0.0", - "sonar-ui-common": "0.0.58", + "sonar-ui-common": "1.0.0", "unist-util-visit": "2.0.2", "valid-url": "1.0.9", "whatwg-fetch": "3.0.0" @@ -147,55 +147,6 @@ "last 3 Edge versions", "IE 11" ], - "jest": { - "coverageDirectory": "/coverage", - "collectCoverageFrom": [ - "src/main/js/**/*.{ts,tsx,js}" - ], - "coverageReporters": [ - "lcovonly", - "text" - ], - "globals": { - "ts-jest": { - "diagnostics": { - "ignoreCodes": [ - 151001 - ] - } - } - }, - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json" - ], - "moduleNameMapper": { - "^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js", - "^.+\\.css$": "/config/jest/CSSStub.js", - "^Docs/@types/types$": "/../sonar-docs/src/@types/types.d.ts", - "^Docs/(.*)": "/../sonar-docs/src/$1" - }, - "setupFiles": [ - "/config/polyfills.js", - "/config/jest/SetupTestEnvironment.js", - "/config/jest/SetupEnzyme.js" - ], - "snapshotSerializers": [ - "enzyme-to-json/serializer" - ], - "testPathIgnorePatterns": [ - "/config", - "/node_modules", - "/scripts" - ], - "testRegex": "(/__tests__/.*|\\-test)\\.(ts|tsx|js)$", - "transform": { - "\\.js$": "babel-jest", - "\\.(ts|tsx)$": "ts-jest" - } - }, "prettier": { "jsxBracketSameLine": true, "printWidth": 100, diff --git a/server/sonar-web/src/main/js/api/l10n.ts b/server/sonar-web/src/main/js/api/l10n.ts new file mode 100644 index 00000000000..fd1bfceca38 --- /dev/null +++ b/server/sonar-web/src/main/js/api/l10n.ts @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { getJSON } from 'sonar-ui-common/helpers/request'; +import { L10nBundleRequestParams, L10nBundleRequestResponse } from '../types/l10n'; + +// eslint-disable-next-line import/prefer-default-export +export function fetchL10nBundle( + params: L10nBundleRequestParams +): Promise { + return getJSON('/api/l10n/index', params); +} diff --git a/server/sonar-web/src/main/js/api/report.ts b/server/sonar-web/src/main/js/api/report.ts index 6d0afee2cf2..e9e933d24ad 100644 --- a/server/sonar-web/src/main/js/api/report.ts +++ b/server/sonar-web/src/main/js/api/report.ts @@ -19,6 +19,7 @@ */ import { getJSON, post } from 'sonar-ui-common/helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; +import { getBaseUrl } from '../helpers/system'; export interface ReportStatus { canDownload?: boolean; @@ -35,11 +36,9 @@ export function getReportStatus(component: string): Promise { } export function getReportUrl(component: string): string { - return ( - (window as any).baseUrl + - '/api/governance_reports/download?componentKey=' + - encodeURIComponent(component) - ); + return `${getBaseUrl()}/api/governance_reports/download?componentKey=${encodeURIComponent( + component + )}`; } export function subscribe(component: string): Promise { diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx index 51c2012aeb7..0978456a9e6 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx @@ -17,10 +17,13 @@ * 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 { isOfficial } from '../../helpers/system'; export default function GlobalFooterBranding() { - const { official } = window as any; + const official = isOfficial(); + return official ? (
SonarQube™ technology is powered by{' '} diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx index dc1a7cc5c79..7ea43850dc8 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx @@ -24,8 +24,11 @@ import { connect } from 'react-redux'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { getExtensionStart } from '../../../helpers/extensions'; +import { getCurrentL10nBundle } from '../../../helpers/l10n'; +import { getBaseUrl } from '../../../helpers/system'; import { addGlobalErrorMessage } from '../../../store/globalMessages'; import { getCurrentUser, Store } from '../../../store/rootReducer'; +import { ExtensionStartMethod } from '../../../types/extension'; import * as theme from '../../theme'; import getStore from '../../utils/getStore'; @@ -64,7 +67,7 @@ export class Extension extends React.PureComponent { this.stopExtension(); } - handleStart = (start: Function) => { + handleStart = (start: ExtensionStartMethod) => { const store = getStore(); const result = start({ store, @@ -74,13 +77,17 @@ export class Extension extends React.PureComponent { location: this.props.location, router: this.props.router, theme, + baseUrl: getBaseUrl(), + l10nBundle: getCurrentL10nBundle(), ...this.props.options }); - if (React.isValidElement(result)) { - this.setState({ extensionElement: result }); - } else { - this.stop = result; + if (result) { + if (React.isValidElement(result)) { + this.setState({ extensionElement: result }); + } else if (typeof result === 'function') { + this.stop = result; + } } }; diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts index 4ade1fb45e0..cf8311856f8 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts +++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts @@ -66,6 +66,7 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import DuplicationsRating from 'sonar-ui-common/components/ui/DuplicationsRating'; import Level from 'sonar-ui-common/components/ui/Level'; import Rating from 'sonar-ui-common/components/ui/Rating'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; import NotFound from '../../../app/components/NotFound'; import Favorite from '../../../components/controls/Favorite'; @@ -175,6 +176,9 @@ const exposeLibraries = () => { Tooltip, VulnerabilityIcon }; + + global.t = translate; + global.tp = translateWithParameters; }; export default exposeLibraries; diff --git a/server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts b/server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts index a8f0fe0ccf3..706d584ff42 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts +++ b/server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts @@ -17,10 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { isNil, omitBy } from 'lodash'; import { stringify } from 'querystring'; -import { omitBy, isNil } from 'lodash'; import { getCookie } from 'sonar-ui-common/helpers/cookies'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import { getBaseUrl } from '../../../../helpers/system'; /* WARNING /!\ WARNING @@ -118,7 +119,7 @@ class Request { submit(): Promise { const { url, options } = this.getSubmitData({ ...getCSRFToken() }); - return window.fetch(((window as any).baseUrl as string) + url, options); + return window.fetch(getBaseUrl() + url, options); } setMethod(method: string): Request { diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index c1e3aecaa9f..854e32fda3d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -25,6 +25,7 @@ import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; import ContextNavBar from 'sonar-ui-common/components/ui/ContextNavBar'; import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; import { PluginPendingResult } from '../../../../api/plugins'; import { rawSizes } from '../../../theme'; import PendingPluginsActionNotif from './PendingPluginsActionNotif'; @@ -47,7 +48,7 @@ export default class SettingsNav extends React.PureComponent { isSomethingActive(urls: string[]): boolean { const path = window.location.pathname; - return urls.some((url: string) => path.indexOf((window as any).baseUrl + url) === 0); + return urls.some((url: string) => path.indexOf(getBaseUrl() + url) === 0); } isSecurityActive() { diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index 6d3f1e848d4..715df897aa9 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -17,21 +17,24 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { DEFAULT_LANGUAGE, installGlobal, requestMessages } from 'sonar-ui-common/helpers/l10n'; + +import SonarUiCommonInitializer from 'sonar-ui-common/helpers/init'; import { parseJSON, request } from 'sonar-ui-common/helpers/request'; import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers/extensionsHandler'; -import { getSystemStatus } from '../helpers/system'; +import { loadL10nBundle } from '../helpers/l10n'; +import { getBaseUrl, getSystemStatus } from '../helpers/system'; import './styles/sonar.css'; -installGlobal(); +SonarUiCommonInitializer.setUrlContext(getBaseUrl()); + installWebAnalyticsHandler(); if (isMainApp()) { installExtensionsHandler(); - Promise.all([loadMessages(), loadUser(), loadAppState(), loadApp()]).then( - ([lang, user, appState, startReactApp]) => { - startReactApp(lang, user, appState); + Promise.all([loadL10nBundle(), loadUser(), loadAppState(), loadApp()]).then( + ([l10nBundle, user, appState, startReactApp]) => { + startReactApp(l10nBundle.locale, user, appState); }, error => { if (isResponse(error) && error.status === 401) { @@ -50,9 +53,9 @@ if (isMainApp()) { .catch(() => resolve(undefined)) ); - Promise.all([loadMessages(), appStatePromise, loadApp()]).then( - ([lang, appState, startReactApp]) => { - startReactApp(lang, undefined, appState); + Promise.all([loadL10nBundle(), appStatePromise, loadApp()]).then( + ([l10nBundle, appState, startReactApp]) => { + startReactApp(l10nBundle.locale, undefined, appState); }, error => { logError(error); @@ -60,31 +63,6 @@ if (isMainApp()) { ); } -function loadMessages() { - return requestMessages().then(setLanguage, setLanguage); -} - -function loadLocaleData(langToLoad: string) { - return Promise.all([import('react-intl/locale-data/' + langToLoad), import('react-intl')]).then( - ([intlBundle, intl]) => { - intl.addLocaleData(intlBundle.default); - } - ); -} - -function setLanguage(lang: string) { - const langToLoad = lang || DEFAULT_LANGUAGE; - // No need to load english (default) bundle, it's coming with react-intl - if (langToLoad !== DEFAULT_LANGUAGE) { - return loadLocaleData(langToLoad).then( - () => langToLoad, - () => DEFAULT_LANGUAGE - ); - } else { - return DEFAULT_LANGUAGE; - } -} - function loadUser() { return request('/api/users/current') .submit() @@ -137,7 +115,3 @@ function isMainApp() { !pathname.startsWith(`${getBaseUrl()}/markdown/help`) ); } - -function getBaseUrl(): string { - return (window as any).baseUrl; -} diff --git a/server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts index 4aefa8441de..81e419fe7c5 100644 --- a/server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts @@ -17,21 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { resetBundle } from 'sonar-ui-common/helpers/l10n'; + +import SonarUiCommonInitializer from 'sonar-ui-common/helpers/init'; import { isSonarCloud } from '../../../helpers/system'; import { convertToPermissionDefinitions } from '../utils'; jest.mock('../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); afterEach(() => { - resetBundle({}); + SonarUiCommonInitializer.setMessages({}); }); describe('convertToPermissionDefinitions', () => { it('should convert and translate a permission definition', () => { (isSonarCloud as jest.Mock).mockImplementation(() => false); - resetBundle({ + SonarUiCommonInitializer.setMessages({ 'global_permissions.admin': 'Administer System' }); @@ -46,7 +47,7 @@ describe('convertToPermissionDefinitions', () => { it('should convert and translate a permission definition for SonarCloud', () => { (isSonarCloud as jest.Mock).mockImplementation(() => true); - resetBundle({ + SonarUiCommonInitializer.setMessages({ 'global_permissions.admin': 'Administer System', 'global_permissions.admin.sonarcloud': 'Administer Organization' }); @@ -66,7 +67,7 @@ describe('convertToPermissionDefinitions', () => { it('should fallback to basic message when SonarCloud version does not exist', () => { (isSonarCloud as jest.Mock).mockImplementation(() => true); - resetBundle({ + SonarUiCommonInitializer.setMessages({ 'global_permissions.admin': 'Administer System' }); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx index d67f52c2c8f..7dd89ec8cc2 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx @@ -19,6 +19,8 @@ */ import * as React from 'react'; import * as theme from '../../../app/theme'; +import { getCurrentL10nBundle } from '../../../helpers/l10n'; +import { getBaseUrl } from '../../../helpers/system'; interface Props { defaultQualifier?: string; @@ -29,6 +31,10 @@ interface Props { export default class CreateFormShim extends React.Component { render() { const { createFormBuilder } = (window as any).SonarGovernance; - return createFormBuilder(this.props, theme); + return createFormBuilder(this.props, { + theme, + baseUrl: getBaseUrl(), + l10nBundle: getCurrentL10nBundle() + }); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 16b620cf03f..aa960dad0a9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -25,6 +25,7 @@ import ActionsDropdown, { import { translate } from 'sonar-ui-common/helpers/l10n'; import { getQualityProfileBackupUrl, setDefaultProfile } from '../../../api/quality-profiles'; import { Router, withRouter } from '../../../components/hoc/withRouter'; +import { getBaseUrl } from '../../../helpers/system'; import { getRulesUrl } from '../../../helpers/urls'; import { Profile } from '../types'; import { getProfileComparePath, getProfilePath, getProfilesPath } from '../utils'; @@ -137,7 +138,7 @@ export class ProfileActions extends React.PureComponent { const { profile } = this.props; const { actions = {} } = profile; - const backupUrl = `${(window as any).baseUrl}${getQualityProfileBackupUrl(profile)}`; + const backupUrl = `${getBaseUrl()}${getQualityProfileBackupUrl(profile)}`; const activateMoreUrl = getRulesUrl( { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx index e324d1cdbf2..381a7c3ad70 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { getQualityProfileExporterUrl } from '../../../api/quality-profiles'; +import { getBaseUrl } from '../../../helpers/system'; import { Exporter, Profile } from '../types'; interface Props { @@ -31,7 +32,7 @@ interface Props { export default class ProfileExporters extends React.PureComponent { getExportUrl(exporter: Exporter) { const { profile } = this.props; - return `${(window as any).baseUrl}${getQualityProfileExporterUrl(exporter, profile)}`; + return `${getBaseUrl()}${getQualityProfileExporterUrl(exporter, profile)}`; } render() { diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx index b21c3e75357..37511fc189f 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx @@ -27,14 +27,6 @@ import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; import DateInput from '../DateInput'; -jest.mock('sonar-ui-common/components/lazyLoad', () => ({ - lazyLoad: () => { - return function DayPicker() { - return null; - }; - } -})); - beforeAll(() => { Date.prototype.getFullYear = jest.fn().mockReturnValue(2018); // eslint-disable-line no-extend-native }); 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 new file mode 100644 index 00000000000..907b8672620 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 reactIntl from 'react-intl'; +import SonarUiCommonInitializer from 'sonar-ui-common/helpers/init'; +import { get } from 'sonar-ui-common/helpers/storage'; +import { fetchL10nBundle } from '../../api/l10n'; +import { loadL10nBundle } from '../l10n'; + +beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(window.navigator, 'languages', 'get').mockReturnValue(['de']); +}); + +jest.mock('../../api/l10n', () => ({ + fetchL10nBundle: jest + .fn() + .mockResolvedValue({ effectiveLocale: 'de', messages: { test_message: 'test' } }) +})); + +jest.mock('sonar-ui-common/helpers/storage', () => ({ + get: jest.fn(), + save: jest.fn() +})); + +describe('#loadL10nBundle', () => { + it('should fetch bundle without any timestamp', async () => { + await loadL10nBundle(); + + expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined }); + }); + + it('should ftech bundle without local storage timestamp if locales are different', async () => { + const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } }; + (get as jest.Mock).mockReturnValueOnce(JSON.stringify(cachedBundle)); + + await loadL10nBundle(); + + expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined }); + }); + + it('should fetch bundle with cached bundle timestamp and browser locale', async () => { + const cachedBundle = { timestamp: 'timestamp', locale: 'de', messages: { cache: 'cache' } }; + (get as jest.Mock).mockReturnValueOnce(JSON.stringify(cachedBundle)); + + await loadL10nBundle(); + + expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: cachedBundle.timestamp }); + }); + + it('should fallback to cached bundle if the server respond with 304', async () => { + const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } }; + (fetchL10nBundle as jest.Mock).mockRejectedValueOnce({ status: 304 }); + (get as jest.Mock).mockReturnValueOnce(JSON.stringify(cachedBundle)); + + const bundle = await loadL10nBundle(); + + expect(bundle).toEqual( + expect.objectContaining({ locale: cachedBundle.locale, messages: cachedBundle.messages }) + ); + }); + + it('should init react-intl & sonar-ui-common', async () => { + jest.spyOn(SonarUiCommonInitializer, 'setLocale'); + jest.spyOn(SonarUiCommonInitializer, 'setMessages'); + jest.spyOn(reactIntl, 'addLocaleData'); + + await loadL10nBundle(); + + expect(SonarUiCommonInitializer.setLocale).toHaveBeenCalledWith('de'); + expect(SonarUiCommonInitializer.setMessages).toHaveBeenCalledWith({ test_message: 'test' }); + expect(reactIntl.addLocaleData).toHaveBeenCalled(); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/browser.ts b/server/sonar-web/src/main/js/helpers/browser.ts new file mode 100644 index 00000000000..e53f59d2f4d --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/browser.ts @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { EnhancedWindow } from '../types/browser'; + +// eslint-disable-next-line import/prefer-default-export +export function getEnhancedWindow() { + return (window as unknown) as EnhancedWindow; +} diff --git a/server/sonar-web/src/main/js/helpers/extensionsHandler.ts b/server/sonar-web/src/main/js/helpers/extensionsHandler.ts index 70ff41679c0..8f99162b133 100644 --- a/server/sonar-web/src/main/js/helpers/extensionsHandler.ts +++ b/server/sonar-web/src/main/js/helpers/extensionsHandler.ts @@ -19,11 +19,14 @@ */ // Do not import dependencies in this helper, to keep initial bundle load as small as possible +import { ExtensionStartMethod } from '../types/extension'; +import { getEnhancedWindow } from './browser'; + const WEB_ANALYTICS_EXTENSION = 'sq-web-analytics'; -const extensions: T.Dict = {}; +const extensions: T.Dict = {}; -function registerExtension(key: string, start: Function) { +function registerExtension(key: string, start: ExtensionStartMethod) { extensions[key] = start; } @@ -32,11 +35,11 @@ function setWebAnalyticsPageChangeHandler(pageHandler: (pathname: string) => voi } export function installExtensionsHandler() { - (window as any).registerExtension = registerExtension; + getEnhancedWindow().registerExtension = registerExtension; } export function installWebAnalyticsHandler() { - (window as any).setWebAnalyticsPageChangeHandler = setWebAnalyticsPageChangeHandler; + getEnhancedWindow().setWebAnalyticsPageChangeHandler = setWebAnalyticsPageChangeHandler; } export function getExtensionFromCache(key: string): Function | undefined { diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts new file mode 100644 index 00000000000..32d46e4f13c --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -0,0 +1,115 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { toNotSoISOString } from 'sonar-ui-common/helpers/dates'; +import SonarUiCommonInitializer, { DEFAULT_LOCALE } from 'sonar-ui-common/helpers/init'; +import { + get as loadFromLocalStorage, + save as saveInLocalStorage +} from 'sonar-ui-common/helpers/storage'; +import { fetchL10nBundle } from '../api/l10n'; +import { L10nBundle, L10nBundleRequestParams } from '../types/l10n'; + +const L10N_BUNDLE_LS_KEY = 'l10n.bundle'; + +export async function loadL10nBundle() { + const bundle = await getLatestL10nBundle().catch(() => ({ + locale: DEFAULT_LOCALE, + messages: {} + })); + + SonarUiCommonInitializer.setLocale(bundle.locale).setMessages(bundle.messages); + // No need to load english (default) bundle, it's coming with react-intl + if (bundle.locale !== DEFAULT_LOCALE) { + const [intlBundle, intl] = await Promise.all([ + import(`react-intl/locale-data/${bundle.locale}`), + import('react-intl') + ]); + + intl.addLocaleData(intlBundle.default); + } + + return bundle; +} + +export async function getLatestL10nBundle() { + const browserLocale = getPreferredLanguage(); + const cachedBundle = loadL10nBundleFromLocalStorage(); + + const params: L10nBundleRequestParams = {}; + + if (browserLocale) { + params.locale = browserLocale; + + if ( + cachedBundle.locale && + browserLocale.startsWith(cachedBundle.locale) && + cachedBundle.timestamp && + cachedBundle.messages + ) { + params.ts = cachedBundle.timestamp; + } + } + + const { effectiveLocale, messages } = await fetchL10nBundle(params).catch(response => { + if (response && response.status === 304) { + return { + effectiveLocale: cachedBundle.locale || browserLocale || DEFAULT_LOCALE, + messages: cachedBundle.messages ?? {} + }; + } else { + throw new Error(`Unexpected status code: ${response.status}`); + } + }); + + const bundle = { + timestamp: toNotSoISOString(new Date()), + locale: effectiveLocale, + messages + }; + + saveL10nBundleToLocalStorage(bundle); + + return bundle; +} + +export function getCurrentL10nBundle() { + return loadL10nBundleFromLocalStorage(); +} + +function getPreferredLanguage() { + return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; +} + +function loadL10nBundleFromLocalStorage() { + let bundle: L10nBundle; + + try { + bundle = JSON.parse(loadFromLocalStorage(L10N_BUNDLE_LS_KEY) ?? '{}'); + } catch { + bundle = {}; + } + + return bundle; +} + +function saveL10nBundleToLocalStorage(bundle: L10nBundle) { + saveInLocalStorage(L10N_BUNDLE_LS_KEY, JSON.stringify(bundle)); +} diff --git a/server/sonar-web/src/main/js/helpers/system.ts b/server/sonar-web/src/main/js/helpers/system.ts index a702f347dad..ca070226262 100644 --- a/server/sonar-web/src/main/js/helpers/system.ts +++ b/server/sonar-web/src/main/js/helpers/system.ts @@ -17,14 +17,26 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export function getSystemStatus(): T.SysStatus { - return (window as any).serverStatus; + +import { InstanceType } from '../types/system'; +import { getEnhancedWindow } from './browser'; + +export function getBaseUrl() { + return getEnhancedWindow().baseUrl; +} + +export function getSystemStatus() { + return getEnhancedWindow().serverStatus; +} + +export function getInstance() { + return getEnhancedWindow().instance; } -export function getInstance(): 'SonarQube' | 'SonarCloud' { - return (window as any).instance; +export function isOfficial() { + return getEnhancedWindow().official; } export function isSonarCloud() { - return getInstance() === 'SonarCloud'; + return getInstance() === InstanceType.SonarCloud; } diff --git a/server/sonar-web/src/main/js/types/browser.ts b/server/sonar-web/src/main/js/types/browser.ts new file mode 100644 index 00000000000..1ce3cf05a05 --- /dev/null +++ b/server/sonar-web/src/main/js/types/browser.ts @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { ExtensionStartMethod } from './extension'; +import { InstanceType } from './system'; + +export interface EnhancedWindow extends Window { + baseUrl: string; + serverStatus: T.SysStatus; + instance: InstanceType; + official: boolean; + + registerExtension: (key: string, start: ExtensionStartMethod) => void; + setWebAnalyticsPageChangeHandler: (pageHandler: (pathname: string) => void) => void; +} diff --git a/server/sonar-web/src/main/js/types/extension.ts b/server/sonar-web/src/main/js/types/extension.ts new file mode 100644 index 00000000000..1f334bd7c8e --- /dev/null +++ b/server/sonar-web/src/main/js/types/extension.ts @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { InjectedIntl } from 'react-intl'; +import { Store as ReduxStore } from 'redux'; +import { Theme } from 'sonar-ui-common/components/theme'; +import { Location, Router } from '../components/hoc/withRouter'; +import { Store } from '../store/rootReducer'; +import { L10nBundle } from './l10n'; + +export interface ExtensionStartMethod { + (params: ExtensionStartMethodParameter | string): ExtensionStartMethodReturnType; +} + +export interface ExtensionStartMethodParameter { + store: ReduxStore; + el: HTMLElement | undefined | null; + currentUser: T.CurrentUser; + intl: InjectedIntl; + location: Location; + router: Router; + theme: Theme; + baseUrl: string; + l10nBundle: L10nBundle; +} + +export type ExtensionStartMethodReturnType = React.ReactNode | Function | void | undefined | null; diff --git a/server/sonar-web/src/main/js/types/l10n.ts b/server/sonar-web/src/main/js/types/l10n.ts new file mode 100644 index 00000000000..0284e51fcf0 --- /dev/null +++ b/server/sonar-web/src/main/js/types/l10n.ts @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ + +export interface L10nBundleRequestParams { + locale?: string; + ts?: string; +} + +export interface L10nBundleRequestResponse { + effectiveLocale: string; + messages: T.Dict; +} + +export interface L10nBundle { + timestamp?: string; + locale?: string; + messages?: T.Dict; +} diff --git a/server/sonar-web/src/main/js/types/system.ts b/server/sonar-web/src/main/js/types/system.ts index aefa28fa2a6..e050a09fb42 100644 --- a/server/sonar-web/src/main/js/types/system.ts +++ b/server/sonar-web/src/main/js/types/system.ts @@ -30,3 +30,8 @@ export interface SystemUpgrade extends SystemUpgradeDownloadUrls { releaseDate?: string; version: string; } + +export enum InstanceType { + SonarQube = 'SonarQube', + SonarCloud = 'SonarCloud' +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 6e8efae570a..65dfc107c10 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -10522,10 +10522,10 @@ sockjs@0.3.19: faye-websocket "^0.10.0" uuid "^3.0.1" -sonar-ui-common@0.0.58: - version "0.0.58" - resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-0.0.58.tgz#860440bd476d176c71828e9b82e193384cd57f66" - integrity sha1-hgRAvUdtF2xxgo6bguGTOEzVf2Y= +sonar-ui-common@1.0.0: + version "1.0.0" + resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-1.0.0.tgz#060bce001925fcce1b86696058819941d3883c63" + integrity sha1-BgvOABkl/M4bhmlgWIGZQdOIPGM= dependencies: "@types/react-select" "1.2.6" classnames "2.2.6"