aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2020-05-15 09:15:38 +0200
committersonartech <sonartech@sonarsource.com>2020-05-27 20:05:46 +0000
commitfd91b02d132511a2436722d77161f746d01910cd (patch)
tree0a323109f558f3e41d3f05d3445ed0553c133f1e /server
parent5e9d00966008decae9ae040fa6701ee7e9da0545 (diff)
downloadsonarqube-fd91b02d132511a2436722d77161f746d01910cd.tar.gz
sonarqube-fd91b02d132511a2436722d77161f746d01910cd.zip
Bump to sonar-ui-common@1.0.0
Diffstat (limited to 'server')
-rw-r--r--server/sonar-docs/package.json2
-rw-r--r--server/sonar-docs/yarn.lock8
-rw-r--r--server/sonar-web/config/jest/SetupTestEnvironment.ts (renamed from server/sonar-web/config/jest/SetupTestEnvironment.js)13
-rw-r--r--server/sonar-web/jest.config.js32
-rw-r--r--server/sonar-web/package.json51
-rw-r--r--server/sonar-web/src/main/js/api/l10n.ts29
-rw-r--r--server/sonar-web/src/main/js/api/report.ts9
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx5
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/Extension.tsx17
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts4
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts5
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/index.ts50
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts11
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts91
-rw-r--r--server/sonar-web/src/main/js/helpers/browser.ts26
-rw-r--r--server/sonar-web/src/main/js/helpers/extensionsHandler.ts11
-rw-r--r--server/sonar-web/src/main/js/helpers/l10n.ts115
-rw-r--r--server/sonar-web/src/main/js/helpers/system.ts22
-rw-r--r--server/sonar-web/src/main/js/types/browser.ts32
-rw-r--r--server/sonar-web/src/main/js/types/extension.ts44
-rw-r--r--server/sonar-web/src/main/js/types/l10n.ts35
-rw-r--r--server/sonar-web/src/main/js/types/system.ts5
-rw-r--r--server/sonar-web/yarn.lock8
28 files changed, 509 insertions, 141 deletions
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.ts
index bfd615891b6..e60b3048b4c 100644
--- a/server/sonar-web/config/jest/SetupTestEnvironment.js
+++ b/server/sonar-web/config/jest/SetupTestEnvironment.ts
@@ -17,12 +17,15 @@
* 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('.');
-};
+
+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: '<rootDir>/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)$':
+ '<rootDir>/config/jest/FileStub.js',
+ '^.+\\.css$': '<rootDir>/config/jest/CSSStub.js',
+ '^Docs/@types/types$': '<rootDir>/../sonar-docs/src/@types/types.d.ts',
+ '^Docs/(.*)': '<rootDir>/../sonar-docs/src/$1'
+ },
+ setupFiles: [
+ '<rootDir>/config/polyfills.js',
+ '<rootDir>/config/jest/SetupEnzyme.js',
+ '<rootDir>/config/jest/SetupTestEnvironment.ts'
+ ],
+ snapshotSerializers: ['enzyme-to-json/serializer'],
+ testPathIgnorePatterns: ['<rootDir>/config', '<rootDir>/node_modules', '<rootDir>/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": "<rootDir>/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)$": "<rootDir>/config/jest/FileStub.js",
- "^.+\\.css$": "<rootDir>/config/jest/CSSStub.js",
- "^Docs/@types/types$": "<rootDir>/../sonar-docs/src/@types/types.d.ts",
- "^Docs/(.*)": "<rootDir>/../sonar-docs/src/$1"
- },
- "setupFiles": [
- "<rootDir>/config/polyfills.js",
- "<rootDir>/config/jest/SetupTestEnvironment.js",
- "<rootDir>/config/jest/SetupEnzyme.js"
- ],
- "snapshotSerializers": [
- "enzyme-to-json/serializer"
- ],
- "testPathIgnorePatterns": [
- "<rootDir>/config",
- "<rootDir>/node_modules",
- "<rootDir>/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<L10nBundleRequestResponse> {
+ 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<ReportStatus> {
}
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<void | Response> {
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 ? (
<div>
SonarQube&trade; 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<Props, State> {
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<Props, State> {
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<Response> {
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<Props> {
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<Props> {
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<Props, State> {
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<Props> {
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<Function> = {};
+const extensions: T.Dict<ExtensionStartMethod> = {};
-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<Store, any>;
+ 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<string>;
+}
+
+export interface L10nBundle {
+ timestamp?: string;
+ locale?: string;
+ messages?: T.Dict<string>;
+}
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"