aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2021-12-27 17:20:42 +0100
committersonartech <sonartech@sonarsource.com>2021-12-28 20:02:47 +0000
commit0bffaef9955651d160502c5df77c8f4a859778b9 (patch)
tree311a9b669f86806d0b01e61b4ae59f7699d3a1b4
parenta9ebb3dd0e2a3188e675757005372ae823f3d23c (diff)
downloadsonarqube-0bffaef9955651d160502c5df77c8f4a859778b9.tar.gz
sonarqube-0bffaef9955651d160502c5df77c8f4a859778b9.zip
SONAR-15861 Allow extensions to ship with a CSS file
-rw-r--r--server/sonar-docs/src/pages/extend/extend-web-app.md4
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap4
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts45
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/extensionsHandler-test.ts4
-rw-r--r--server/sonar-web/src/main/js/helpers/extensions.ts28
-rw-r--r--server/sonar-web/src/main/js/helpers/extensionsHandler.ts12
-rw-r--r--server/sonar-web/src/main/js/types/extension.ts5
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;
}