diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-12-27 17:20:42 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-12-28 20:02:47 +0000 |
commit | 0bffaef9955651d160502c5df77c8f4a859778b9 (patch) | |
tree | 311a9b669f86806d0b01e61b4ae59f7699d3a1b4 | |
parent | a9ebb3dd0e2a3188e675757005372ae823f3d23c (diff) | |
download | sonarqube-0bffaef9955651d160502c5df77c8f4a859778b9.tar.gz sonarqube-0bffaef9955651d160502c5df77c8f4a859778b9.zip |
SONAR-15861 Allow extensions to ship with a CSS file
7 files changed, 82 insertions, 20 deletions
diff --git a/server/sonar-docs/src/pages/extend/extend-web-app.md b/server/sonar-docs/src/pages/extend/extend-web-app.md index aa090059f3f..28a301ca54d 100644 --- a/server/sonar-docs/src/pages/extend/extend-web-app.md +++ b/server/sonar-docs/src/pages/extend/extend-web-app.md @@ -103,6 +103,10 @@ The `options` object will contain the following: [[info]] | SonarQube doesn't guarantee any JavaScript library availability at runtime (except React). If you need a library, include it in the final file. +### CSS files + +If you want a static CSS file to be loaded when your extension is bootstrapped, rather than using run-time inclusion of styles, you can pass `true` as a third parameter to the `window.registerExtension()` function. This will trigger the loading of a CSS file that *must* have the same basename as the registering JS file. I.e., if your extension JS file is `/static/global_page.js`, the CSS file must be called `/static/global_page.css`. The bootstrap will wait for the CSS file to be fully loaded before calling the *start* callback. + ## Examples It is highly recommended you check out [sonar-custom-plugin-example](https://github.com/SonarSource/sonar-custom-plugin-example/tree/7.x/). It contains detailed examples using several front-end frameworks, and its code is thoroughly documented. It also describes how to run a local development server to speed up the front-end development, without requiring a full rebuild and re-deploy to test your changes. diff --git a/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap index f535eda398a..009678f3d22 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap +++ b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap @@ -1,3 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`installScript should add the given script in the dom 1`] = `"<script src=\\"custom_script.js\\"></script>"`; +exports[`installScript should add the given script to the dom 1`] = `"<script src=\\"custom_script.js\\"></script>"`; + +exports[`installStyles should add the given stylesheet to the dom 1`] = `"<link href=\\"custom_styles.css\\" rel=\\"stylesheet\\">"`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts index 20df8a7491e..a26c3ebfd20 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts @@ -18,27 +18,56 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import exposeLibraries from '../../app/components/extensions/exposeLibraries'; -import { getExtensionStart, installScript } from '../extensions'; +import { getExtensionStart, installScript, installStyles } from '../extensions'; import { installExtensionsHandler } from '../extensionsHandler'; jest.mock('../../app/components/extensions/exposeLibraries', () => jest.fn()); beforeEach(() => { jest.clearAllMocks(); + document.body.childNodes.forEach(node => document.body.removeChild(node)); + document.head.childNodes.forEach(node => document.head.removeChild(node)); }); describe('installScript', () => { - it('should add the given script in the dom', () => { + it('should add the given script to the dom', () => { installScript('custom_script.js'); expect(document.body.innerHTML).toMatchSnapshot(); }); }); +describe('installStyles', () => { + it('should add the given stylesheet to the dom', async () => { + installStyles('custom_styles.css'); + await new Promise(setImmediate); + expect(document.head.innerHTML).toMatchSnapshot(); + }); +}); + describe('getExtensionStart', () => { + const originalCreateElement = document.createElement; + const scriptTag = document.createElement('script'); + const linkTag = document.createElement('link'); + + beforeEach(() => { + Object.defineProperty(document, 'createElement', { + writable: true, + value: jest + .fn() + .mockReturnValueOnce(scriptTag) + .mockReturnValueOnce(linkTag) + }); + }); + + afterEach(() => { + Object.defineProperty(document, 'createElement', { + writable: true, + value: originalCreateElement + }); + }); + it('should install the extension in the to dom', async () => { const start = jest.fn(); - const scriptTag = document.createElement('script'); - document.createElement = jest.fn().mockReturnValue(scriptTag); installExtensionsHandler(); const result = getExtensionStart('bar'); @@ -46,8 +75,14 @@ describe('getExtensionStart', () => { await new Promise(setImmediate); expect(exposeLibraries).toBeCalled(); - (window as any).registerExtension('bar', start); + (window as any).registerExtension('bar', start, true); + (scriptTag.onload as Function)(); + await new Promise(setImmediate); + + (linkTag.onload as Function)(); + await new Promise(setImmediate); + return expect(result).resolves.toBe(start); }); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/extensionsHandler-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/extensionsHandler-test.ts index 8ead8a82637..936375c23a1 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/extensionsHandler-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/extensionsHandler-test.ts @@ -31,8 +31,8 @@ describe('installExtensionsHandler & extensions.getExtensionFromCache', () => { expect((window as any).registerExtension).toEqual(expect.any(Function)); const start = jest.fn(); - (window as any).registerExtension('foo', start); - expect(getExtensionFromCache('foo')).toBe(start); + (window as any).registerExtension('foo', start, true); + expect(getExtensionFromCache('foo')).toEqual({ start, providesCSSFile: true }); }); }); diff --git a/server/sonar-web/src/main/js/helpers/extensions.ts b/server/sonar-web/src/main/js/helpers/extensions.ts index 7b1ef7286d0..53fde4a9a9b 100644 --- a/server/sonar-web/src/main/js/helpers/extensions.ts +++ b/server/sonar-web/src/main/js/helpers/extensions.ts @@ -18,10 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { getBaseUrl } from '../helpers/system'; +import { ExtensionStartMethod } from '../types/extension'; import { getExtensionFromCache } from './extensionsHandler'; let librariesExposed = false; +export function installStyles(url: string, target: 'body' | 'head' = 'head'): Promise<any> { + return new Promise(resolve => { + const linkTag = document.createElement('link'); + linkTag.href = `${getBaseUrl()}${url}`; + linkTag.rel = 'stylesheet'; + linkTag.onload = resolve; + document.getElementsByTagName(target)[0].appendChild(linkTag); + }); +} + export function installScript(url: string, target: 'body' | 'head' = 'body'): Promise<any> { return new Promise(resolve => { const scriptTag = document.createElement('script'); @@ -31,10 +42,10 @@ export function installScript(url: string, target: 'body' | 'head' = 'body'): Pr }); } -export async function getExtensionStart(key: string) { +export async function getExtensionStart(key: string): Promise<ExtensionStartMethod | undefined> { const fromCache = getExtensionFromCache(key); if (fromCache) { - return Promise.resolve(fromCache); + return Promise.resolve(fromCache.start); } if (!librariesExposed) { @@ -46,9 +57,14 @@ export async function getExtensionStart(key: string) { await installScript(`/static/${key}.js`); - const start = getExtensionFromCache(key); - if (start) { - return start; + const extension = getExtensionFromCache(key); + if (!extension) { + return Promise.reject(); } - return Promise.reject(); + + if (extension.providesCSSFile) { + await installStyles(`/static/${key}.css`); + } + + return extension.start; } diff --git a/server/sonar-web/src/main/js/helpers/extensionsHandler.ts b/server/sonar-web/src/main/js/helpers/extensionsHandler.ts index 88e50f2b4d4..078cc9ca6e3 100644 --- a/server/sonar-web/src/main/js/helpers/extensionsHandler.ts +++ b/server/sonar-web/src/main/js/helpers/extensionsHandler.ts @@ -19,15 +19,15 @@ */ // Do not import dependencies in this helper, to keep initial bundle load as small as possible -import { ExtensionStartMethod } from '../types/extension'; +import { ExtensionRegistryEntry, ExtensionStartMethod } from '../types/extension'; import { getEnhancedWindow } from './browser'; const WEB_ANALYTICS_EXTENSION = 'sq-web-analytics'; -const extensions: T.Dict<ExtensionStartMethod> = {}; +const extensions: T.Dict<ExtensionRegistryEntry> = {}; -function registerExtension(key: string, start: ExtensionStartMethod) { - extensions[key] = start; +function registerExtension(key: string, start: ExtensionStartMethod, providesCSSFile = false) { + extensions[key] = { start, providesCSSFile }; } function setWebAnalyticsPageChangeHandler(pageHandler: (pathname: string) => void) { @@ -42,10 +42,10 @@ export function installWebAnalyticsHandler() { getEnhancedWindow().setWebAnalyticsPageChangeHandler = setWebAnalyticsPageChangeHandler; } -export function getExtensionFromCache(key: string): Function | undefined { +export function getExtensionFromCache(key: string): ExtensionRegistryEntry | undefined { return extensions[key]; } export function getWebAnalyticsPageHandlerFromCache(): Function | undefined { - return extensions[WEB_ANALYTICS_EXTENSION]; + return extensions[WEB_ANALYTICS_EXTENSION]?.start; } diff --git a/server/sonar-web/src/main/js/types/extension.ts b/server/sonar-web/src/main/js/types/extension.ts index 38d0432c267..30630dd2fc3 100644 --- a/server/sonar-web/src/main/js/types/extension.ts +++ b/server/sonar-web/src/main/js/types/extension.ts @@ -27,6 +27,11 @@ export enum AdminPageExtension { GovernanceConsole = 'governance/views_console' } +export interface ExtensionRegistryEntry { + start: ExtensionStartMethod; + providesCSSFile: boolean; +} + export interface ExtensionStartMethod { (params: ExtensionStartMethodParameter | string): ExtensionStartMethodReturnType; } |