Browse Source

Bump to sonar-ui-common@1.0.0

tags/8.4.0.35506
Philippe Perrin 4 years ago
parent
commit
fd91b02d13
28 changed files with 509 additions and 141 deletions
  1. 1
    1
      server/sonar-docs/package.json
  2. 4
    4
      server/sonar-docs/yarn.lock
  3. 8
    5
      server/sonar-web/config/jest/SetupTestEnvironment.ts
  4. 32
    0
      server/sonar-web/jest.config.js
  5. 1
    50
      server/sonar-web/package.json
  6. 29
    0
      server/sonar-web/src/main/js/api/l10n.ts
  7. 4
    5
      server/sonar-web/src/main/js/api/report.ts
  8. 4
    1
      server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx
  9. 12
    5
      server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
  10. 4
    0
      server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
  11. 3
    2
      server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts
  12. 2
    1
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
  13. 12
    38
      server/sonar-web/src/main/js/app/index.ts
  14. 6
    5
      server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts
  15. 7
    1
      server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
  16. 2
    1
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
  17. 2
    1
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx
  18. 0
    8
      server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
  19. 91
    0
      server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts
  20. 26
    0
      server/sonar-web/src/main/js/helpers/browser.ts
  21. 7
    4
      server/sonar-web/src/main/js/helpers/extensionsHandler.ts
  22. 115
    0
      server/sonar-web/src/main/js/helpers/l10n.ts
  23. 17
    5
      server/sonar-web/src/main/js/helpers/system.ts
  24. 32
    0
      server/sonar-web/src/main/js/types/browser.ts
  25. 44
    0
      server/sonar-web/src/main/js/types/extension.ts
  26. 35
    0
      server/sonar-web/src/main/js/types/l10n.ts
  27. 5
    0
      server/sonar-web/src/main/js/types/system.ts
  28. 4
    4
      server/sonar-web/yarn.lock

+ 1
- 1
server/sonar-docs/package.json View 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": {

+ 4
- 4
server/sonar-docs/yarn.lock View 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"

server/sonar-web/config/jest/SetupTestEnvironment.js → server/sonar-web/config/jest/SetupTestEnvironment.ts View File

@@ -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);

+ 32
- 0
server/sonar-web/jest.config.js View File

@@ -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'
}
};

+ 1
- 50
server/sonar-web/package.json View 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"
@@ -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,

+ 29
- 0
server/sonar-web/src/main/js/api/l10n.ts View File

@@ -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);
}

+ 4
- 5
server/sonar-web/src/main/js/api/report.ts View 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> {

+ 4
- 1
server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx View File

@@ -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{' '}

+ 12
- 5
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx View 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;
}
}
};


+ 4
- 0
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts View 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;

+ 3
- 2
server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts View File

@@ -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 {

+ 2
- 1
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx View 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() {

+ 12
- 38
server/sonar-web/src/main/js/app/index.ts View File

@@ -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;
}

+ 6
- 5
server/sonar-web/src/main/js/apps/permissions/__tests__/utils-test.ts View File

@@ -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'
});


+ 7
- 1
server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx View 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()
});
}
}

+ 2
- 1
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx View 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(
{

+ 2
- 1
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx View 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() {

+ 0
- 8
server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx View 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
});

+ 91
- 0
server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts View File

@@ -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();
});
});

+ 26
- 0
server/sonar-web/src/main/js/helpers/browser.ts View File

@@ -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;
}

+ 7
- 4
server/sonar-web/src/main/js/helpers/extensionsHandler.ts View File

@@ -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 {

+ 115
- 0
server/sonar-web/src/main/js/helpers/l10n.ts View File

@@ -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));
}

+ 17
- 5
server/sonar-web/src/main/js/helpers/system.ts View File

@@ -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;
}

+ 32
- 0
server/sonar-web/src/main/js/types/browser.ts View File

@@ -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;
}

+ 44
- 0
server/sonar-web/src/main/js/types/extension.ts View File

@@ -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;

+ 35
- 0
server/sonar-web/src/main/js/types/l10n.ts View File

@@ -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>;
}

+ 5
- 0
server/sonar-web/src/main/js/types/system.ts View File

@@ -30,3 +30,8 @@ export interface SystemUpgrade extends SystemUpgradeDownloadUrls {
releaseDate?: string;
version: string;
}

export enum InstanceType {
SonarQube = 'SonarQube',
SonarCloud = 'SonarCloud'
}

+ 4
- 4
server/sonar-web/yarn.lock View 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"

Loading…
Cancel
Save