]> source.dussan.org Git - sonarqube.git/commitdiff
Bump to sonar-ui-common@1.0.0
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Fri, 15 May 2020 07:15:38 +0000 (09:15 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 27 May 2020 20:05:46 +0000 (20:05 +0000)
29 files changed:
server/sonar-docs/package.json
server/sonar-docs/yarn.lock
server/sonar-web/config/jest/SetupTestEnvironment.js [deleted file]
server/sonar-web/config/jest/SetupTestEnvironment.ts [new file with mode: 0644]
server/sonar-web/jest.config.js [new file with mode: 0644]
server/sonar-web/package.json
server/sonar-web/src/main/js/api/l10n.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/report.ts
server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/index.ts
server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx
server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/browser.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/extensionsHandler.ts
server/sonar-web/src/main/js/helpers/l10n.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/system.ts
server/sonar-web/src/main/js/types/browser.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/extension.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/l10n.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/system.ts
server/sonar-web/yarn.lock

index 112755dae67353b0eee8bd6051e88c951540d607..cfdcbc2fa757e48a733bf9f491d6fbfa8297141b 100644 (file)
@@ -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": {
index 9eb9c218575336c43f9d9704e44553db8a3fe6a4..66034d357049a4bba0085d6522ec4b0f7c9303dd 100644 (file)
@@ -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 (file)
index bfd6158..0000000
+++ /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 (file)
index 0000000..e60b304
--- /dev/null
@@ -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 (file)
index 0000000..37a3d45
--- /dev/null
@@ -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'
+  }
+};
index f7b0ff36cf83fabd5cf137566bc10e23a150a682..43696825ec1493438f3710623f33daca7c38910f 100644 (file)
@@ -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"
     "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 (file)
index 0000000..fd1bfce
--- /dev/null
@@ -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);
+}
index 6d0afee2cf244cb3678dcabb1af5edd5bebc5783..e9e933d24ad682fc88cc89658c4572865de0c566 100644 (file)
@@ -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> {
index 51c2012aeb77fdbe23f8ac6aec5bb7e2d2a2871f..0978456a9e60eaecf97da5dd86565095a8cf9fb0 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import * 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{' '}
index dc1a7cc5c795ec67dd45ebf7ff5e6c3bd460de21..7ea43850dc86442d00acb3f7ee8c0eef42e61910 100644 (file)
@@ -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;
+      }
     }
   };
 
index 4ade1fb45e00c03d798c35dac4f1fff18159d7bf..cf8311856f81370dca639c6c2667215eb1f93cc3 100644 (file)
@@ -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;
index a8f0fe0ccf3a3bae402263ba382197ba53984f23..706d584ff427c89070c631c0afa645f375b8ee1c 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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 {
index c1e3aecaa9fab557c7e4900e0027240599b3c057..854e32fda3d61f21f7db8925cba3109886fdd5a1 100644 (file)
@@ -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() {
index 6d3f1e848d4a848df02af9ef973ab8044f9da90c..715df897aa9abe3545111949eb6cdb8ac35af94e 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { 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;
-}
index 4aefa8441de044de6e7ef752470de92710b0b95f..81e419fe7c558729e5f81539a1c432a5e19b3b73 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { 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'
     });
 
index d67f52c2c8f58d910407523cf141115c3bcb7430..7dd89ec8cc2660a005a60d52f6897f744e2ee463 100644 (file)
@@ -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()
+    });
   }
 }
index 16b620cf03f8aab85f5955a31802c568fca9cdba..aa960dad0a9f21a07f22587fc1905d1fec6db72d 100644 (file)
@@ -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(
       {
index e324d1cdbf2b189514182b202c95ddcb64f037e8..381a7c3ad70ea87a0b536bfaeb76ec5f0b29b8ad 100644 (file)
@@ -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() {
index b21c3e75357a82acabfcd6abfffb464845adcc56..37511fc189f17b62c7e6e63d89a3802c8a04f411 100644 (file)
@@ -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 (file)
index 0000000..907b867
--- /dev/null
@@ -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 (file)
index 0000000..e53f59d
--- /dev/null
@@ -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;
+}
index 70ff41679c04c3baf7ea1144c41ea6e5841b442a..8f99162b133cbe5329a6304709540af7ab901b2b 100644 (file)
  */
 // 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 (file)
index 0000000..32d46e4
--- /dev/null
@@ -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));
+}
index a702f347dad8ac560862be2dcd0d56919e5716a8..ca070226262a7f84d0d61da2af08132ad822508b 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-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 (file)
index 0000000..1ce3cf0
--- /dev/null
@@ -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 (file)
index 0000000..1f334bd
--- /dev/null
@@ -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 (file)
index 0000000..0284e51
--- /dev/null
@@ -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>;
+}
index aefa28fa2a645ebec39f0d02defcfc6a3887a69e..e050a09fb42d06ccaab473ef534111a203e13897 100644 (file)
@@ -30,3 +30,8 @@ export interface SystemUpgrade extends SystemUpgradeDownloadUrls {
   releaseDate?: string;
   version: string;
 }
+
+export enum InstanceType {
+  SonarQube = 'SonarQube',
+  SonarCloud = 'SonarCloud'
+}
index 6e8efae570af59d7a01bb856dd16c83da011cf29..65dfc107c1067c2e056402d684bc2f9098d36adb 100644 (file)
@@ -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"