@@ -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": { |
@@ -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" |
@@ -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); |
@@ -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' | |||
} | |||
}; |
@@ -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, |
@@ -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); | |||
} |
@@ -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> { |
@@ -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™ technology is powered by{' '} |
@@ -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; | |||
} | |||
} | |||
}; | |||
@@ -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; |
@@ -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 { |
@@ -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() { |
@@ -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; | |||
} |
@@ -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' | |||
}); | |||
@@ -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() | |||
}); | |||
} | |||
} |
@@ -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( | |||
{ |
@@ -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() { |
@@ -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 | |||
}); |
@@ -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(); | |||
}); | |||
}); |
@@ -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; | |||
} |
@@ -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 { |
@@ -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,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; | |||
} |
@@ -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; | |||
} |
@@ -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; |
@@ -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>; | |||
} |
@@ -30,3 +30,8 @@ export interface SystemUpgrade extends SystemUpgradeDownloadUrls { | |||
releaseDate?: string; | |||
version: string; | |||
} | |||
export enum InstanceType { | |||
SonarQube = 'SonarQube', | |||
SonarCloud = 'SonarCloud' | |||
} |
@@ -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" |