From 8a7c657379d3b8bc89cf68348c99bed1e06ecdde Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Fri, 26 Jul 2019 09:53:03 +0200 Subject: [PATCH] SONAR-12348 Update extension guide documentation --- .../src/pages/extend/extend-web-app.md | 329 +++++------------- 1 file changed, 78 insertions(+), 251 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 2b4fae014d2..3eb6eb7a317 100644 --- a/server/sonar-docs/src/pages/extend/extend-web-app.md +++ b/server/sonar-docs/src/pages/extend/extend-web-app.md @@ -2,279 +2,106 @@ title: Adding pages to the webapp url: /extend/extend-web-app/ --- -SonarQube provides the ability to add a new JavaScript page. A page (or page extension) is a JavaScript application that runs in the SonarQube environment. You can find the example of page extensions in the SonarQube or [sonar-custom-plugin-example](https://github.com/SonarSource/sonar-custom-plugin-example/tree/6.x/) repositories on GitHub. +SonarQube's UI is built as a Single Page Application, using [React](https://reactjs.org/). It provides the ability to add a new pages to the UI using JavaScript. A page (or page extension) is a self-contained JavaScript application that runs in the SonarQube environment. You can find the example of page extensions in the [SonarQube](https://github.com/SonarSource/sonarqube) or [sonar-custom-plugin-example](https://github.com/SonarSource/sonar-custom-plugin-example/tree/7.x/) repositories on GitHub. -## Getting Started -### Step 1. Create a Java class implementing PageDefinition -For each page, you'll need to set a key and a name. The page key should have the format `plugin_key/page_id`. Example: `governance/project_dump`. The `plugin_key` is computed from the `` in your `pom.xml`, or can be set explicitly in the pom using the `` parameter in `sonar-packaging-maven-plugin` configuration. +Before reading this guide, make sure you know how to [build, deploy, and debug a plugin](/extend/developing-plugin/). + +## Step 1. Create a Java class implementing PageDefinition + +For each page, you'll need to set a key and a name. The page key should have the format `plugin_key/page_id` (e.g.: `governance/project_dump`). The `plugin_key` is computed from the `` in your `pom.xml`, or can be set explicitly in the pom using the `` parameter in the `sonar-packaging-maven-plugin` configuration. All the pages should be declared in this class. + +Example: -All the pages should be declared in this class. ``` +import org.sonar.api.web.page.Page; import org.sonar.api.web.page.PageDefinition; +import org.sonar.api.web.page.Context; + +import static org.sonar.api.web.page.Page.Scope.COMPONENT; +import static org.sonar.api.web.page.Page.Qualifier.VIEW; +import static org.sonar.api.web.page.Page.Qualifier.SUB_VIEW; public class MyPluginPageDefinition implements PageDefinition { @Override public void define(Context context) { context - .addPage(Page.builder("my_plugin/my_page").setName("My Page").build()) - .addPage(Page.builder("my_plugin/another_page").setName("Another Page").build()); + .addPage(Page.builder("my_plugin/global_page") + .setName("Global Page") + .build()) + .addPage(Page.builder("my_plugin/project_page") + .setName("Project Page") + .setScope(COMPONENT) + .build()) + .addPage(Page.builder("my_plugin/portfolio_page") + .setName("Portfolio Page") + .setScope(COMPONENT) + .setComponentQualifiers(VIEW, SUB_VIEW) + .build()) + .addPage(Page.builder("my_plugin/admin_page") + .setName("Admin Page") + .setAdmin(true) + .build()); } } ``` -### Step 2. Create a JavaScript file -This file should have the same name as the page key (`my_page.js` in this case) and should be located in `src/main/resources/static`. -``` -// my_page.js -window.registerExtension('my_plugin/my_page', function (options) { - options.el.textContent = 'This is my page!'; - return function () { - options.el.textContent = ''; - }; -}); -``` -Where `my_plugin/my_page` is the same page key specified in step 1. +### Configuring each page -### Configuring the page -There are 3 settings available when you define the page extensions using the PageDefinition class: +There are 3 settings available when you define the page extensions using the `PageDefinition` class: -* `isAdmin`: tells if the page should be restricted to users with the administer permission. -* `scope`: tells if the page should be displayed in the primary menu (`GLOBAL` scope) or inside a component page (`COMPONENT` scope). By default, a page is global. -* `component qualifiers`: allows you to specify if the page should be displayed for `PROJECT`, `MODULE`, `VIEW` or `SUB_VIEW` (the last two come with the Enterprise Edition). If set, the scope of the page must be `COMPONENT`. +* `setAdmin(boolean admin)`: flag this page as restricted to users with "administer" permission. Defaults to `false`. +* `setScope(org.sonar.api.web.page.Page.Scope scope)`: set the scope of this page. Available scopes are `GLOBAL` (default), which will add this page to the main menu, and `COMPONENT`, which will add the page to a project, application, or portfolio menu (applications and portfolios only apply to Enterprise Edition and above). +* `setComponentQualifiers(org.sonar.api.web.page.Qualifier... qualifiers)`: if `setScope()` is set to `COMPONENT`, this sets to what kind of component the page applies to. Available qualifiers are `PROJECT`, `APP`, `VIEW` (portfolio), and `SUB_VIEW` (`APP`, `VIEW`, and `SUB_VIEW` only apply to Enterprise Edition and above). You can pass multiple qualifiers. If no qualifier is set, it will apply to all types. -### Runtime environment -SonarQube provides a global function `registerExtension` which should be called from the main javascript file. The function accepts two parameters: +## Step 2. Create a JavaScript file per page -* page extension key, which has a form of `/` (Ex: `governance/project_dump`) -* callback function, which is executed when the page extension is loaded. This callback should return another function, which will be called once the page extension will be closed. The callback accepts a single parameter options containing: - * `options.el` is a DOM element you must use to put the content inside - * `options.currentUser` contains the response of api/users/current (see Web API docs for details) - * (optional) `options.component` contains the information of the current project or view, if the page is project-level: key, name and qualifier +The `PageDefinition` will register each key as an available route in SonarQube. Whenever this route is visited, SonarQube will asynchronously fetch a single JavaScript file from your plugin's `/static/` directory, and boot up your page's application. This file should have the same name as the `page_id` you defined in your `PageDefinition` class. In the above example, you would need 4 different JavaScript files: -[[info]] -| SonarQube doesn't guarantee any JavaScript library availability at runtime. If you need a library, include it in the final file. +* `/static/global_page.js` +* `/static/project_page.js` +* `/static/portfolio_page.js` +* `/static/admin_page.js` -### Example -Displaying the number of project issues -``` -window.registerExtension('my_plugin/my_page', function (options) { - - // let's create a flag telling if the page is still displayed - var isDisplayed = true; - - // then do a Web API call to the /api/issues/search to get the number of issues - // we pass `resolved: false` to request only unresolved issues - // and `componentKeys: options.component.key` to request issues of the given project - window.SonarRequest.getJSON('/api/issues/search', { - resolved: false, - componentKeys: options.component.key - }).then(function (response) { - - // once the request is done, and the page is still displayed (not closed already) - if (isDisplayed) { - - // let's create an `h2` tag and place the text inside - var header = document.createElement('h2'); - header.textContent = 'The project has ' + response.total + ' issues'; - - // append just created element to the container - options.el.appendChild(header); - } +Each file *must* call the global `window.registerExtension()` function, and pass its *full key* as a first argument (`plugin_key/page_id`, e.g.: `governance/project_dump`). The second argument is the *start* callback. This function will be called once your page is started, and receive information about the current page as an argument (see below). The return value of the start callback depends on how you want to implement your page: + +* If you want to use [React](https://reactjs.org/), you should return a React Component: + ``` + // static/global_page.js + import React from "react"; + import App from "./components/App"; + + window.registerExtension('my_plugin/global_page', function (options) { + return }); - - // return a function, which is called when the page is being closed - return function () { - - // we unset the `isDisplayed` flag to ignore to Web API calls finished after the page is closed - isDisplayed = false; - }; -}); -``` + ``` +* If you want to use any other framework, you should perform any start logic directly inside the start function body, and **return a shutdown callback**: + ``` + // static/global_page.js + const init = require("./my-app/init"); + + window.registerExtension('my_plugin/global_page', function (options) { + // Start up my custom application, passing the DOM element which will serve as + // the container. + init.boot(options.el, options.currentUser, options.component); + + // Whenever the user leaves the page, cleanly shut everything down + // (i.e., remove event listeners, stop running timers, etc). + return function () { + init.removeEventListeners(); + init.clearState(); + init.shutdown(); + }; + }); + ``` -## Implement Pages with React -### Prerequisites -* Be familiar with how to [build, deploy, and debug a plugin](/extend/developing-plugin/) -* Read the Get Started guide of ReactJS: https://reactjs.org/ to be familiar with React Components -* [NodeJS](https://nodejs.org/en/) has to be installed on your developer box +The `options` object will contain the following: +* `options.el`: a DOM node you must use to inject your content. +* `options.currentUser`: information about the current user. +* (optional) `options.component`: contains the information of the current project, application, or portfolio. [[info]] -| SonarQube uses React 15.6.2 in the background. You should not try to use features of React 16+ in Custom Pages. This has not been tested. - - -### Custom Plugin -Everything has been prepared for you to be ready to start coding Custom Pages in this repo: https://github.com/SonarSource/sonar-custom-plugin-example. This way you don't have to spend time with the glue (maven, yarn, npm) and you can concentrate on implementing your pages. - -Clone it and run `mvn clean package`. You will get a deployable JAR file. Once deployed, you will see some new pages at global, project and admin level. This has been done so you see Custom Pages with React in action. - -This plugin contains 2 example pages built with React: - -* https://github.com/SonarSource/sonar-custom-plugin-example/blob/master/src/main/js/app-measures_history.js -* https://github.com/SonarSource/sonar-custom-plugin-example/blob/master/src/main/js/app-sanity_check.js - -### Instance Statistics Page Example -The goal of this page is to show some statistics about your SonarQube instance and so to demonstrate how to call SQ Web API from your custom page inside a React component. - -The page `app-sanity_check.js` is made of only one React Component named `InstanceStatisticsApp`. `InstanceStatisticsApp` is called in the `render()` method and will be in charge of: - -* executing the queries to gather the data -* displaying them. - - -In the `componentDidMount()` method you will retrieve all the method calls to get the data from SonarQube. The various methods such as `findQualityProfilesStatistics` are defined in the `api.js` file. The complexity to gather the information is hidden in the `api.js`: -``` -//InstanceStatisticsApp.js -componentDidMount() { - findQualityProfilesStatistics().then( - (valuesReturnedByAPI) => { - this.setState({ - numberOfQualityProfiles: valuesReturnedByAPI - }); - } - ); - [...] -} -``` -In the render() method we display the information gathered by `componentDidMount()` by mixing HTML and data, aka JSX code. -``` -render() { - return ( -
- - - - - - - -
# Quality Profiles{this.state.numberOfQualityProfiles}
-
- ) -} -``` - -### FAQ - -Q: Can I reuse React Components created by SonarSource to build SonarQube? -A: No, SonarQube is not exposing them, so you will have to build your own React Component - -Q: How can I add my own styles? -A: Feed the `style.css` and reference it in your custom page - - -## Making AJAX requests -All ajax requests must provide CSRF protection token. In order to help you to do so, we provide a set of useful helpers. - -### Getting Started -Let's start with a simple `GET` request -``` -window.SonarRequest - .getJSON('/api/issues/search') - .then(function (response) { - // here 'response' contains the object representing the JSON output - }); -``` -* `window.SonarRequest` contains all the helper methods to do an API requests. -* `window.SonarRequest.getJSON` is a simplest helper to do an API call, receive some data and parse it as JSON. - -### API Documentation -* `window.SonarRequest.request(url: string): Request` -Start making an API call. Return a Request instance which has the following methods: - * `setMethod(method: string)`: Request sets the http method, can be GET, POST, etc. - * `setData(data: object)`: Request sets the request parameters` - * `submit()`: Promise sends the request - -* `window.SonarRequest.getJSON(url: string[, data: object]): Promise` -Send a GET request, get a response, parse it as JSON. - -* `window.SonarRequest.postJSON(url: string[, data: object]): Promise` -Send a POST request, get a response, parse it as JSON. - -* `window.SonarRequest.post(url: string[, data: object]): Promise` -Send a POST request, ignore the response content. - -### Examples -Get the list of unresolved issues -``` -window.SonarRequest.getJSON( - '/api/issues/search', - { resolved: false } -).then(function (response) { - // response.issues contains the list of issues -}); -``` -Create new project -``` -window.SonarRequest.post( - '/api/projects/create', - { key: 'sample', name: 'Sample' } -).then(function () { - // the project has been created -}); -``` -Handle bad requests -``` -window.SonarRequest.post( - '/api/users/deactivate', - { login: 'admin' } -).catch(function (error) { - // error.response.status === 400 - // error.response.statusText === 'Bad Request' - // To read the response: - // error.response.json().then(function (jsonResponse) { ... }); -}); -``` - - - - -## Debugging your page -When you are developing a custom page, if you want to see the impacts of your changes, you have to compile your plugin, deploy it in SonarQube and finally point your browser to that page to see the changes. This process is long, not efficient and doesn't allow you to quickly adjust your code. - -The easiest way to shorten the loop is to setup an HTTP proxy working like this: - -* each time you will request a standard SonarQube URL, the proxy will redirect the call to SonarQube itself -* when you will request your page in the browser, the proxy will server your JS file from your local box from the path you are currently developing it. - -In this example, we are going to use a JS implementation for the HTTP proxy based on Node.js: https://github.com/nodejitsu/node-http-proxy - -**Requirements** -* Node.js LTS 6.11+ -* HTTP Proxy 1.16.2 for Node.js: https://github.com/nodejitsu/node-http-proxy (to be installed in the next step) -* Your custom plugin containing the custom page you want to debug has to be deployed at least once in SonarQube - -### Running the Proxy -Once Node is installed, check you can run it using : `node --version` - -Install HTTP Proxy using this command: `npm install http-proxy --save` - -Then put the following code in a file named `sq-proxy.js`: -``` -var http = require('http'), - httpProxy = require('http-proxy'), - fs = require('fs'); -var proxy = httpProxy.createProxyServer({}); -var server = http.createServer(function(req, res) { - //console.log(req.url); - if (req.url === '/static/example/custom_page_global.js') { - res.writeHead(200, { 'Content-Type': 'application/javascript' }); - fs.readFile('/Users/.../sonar-custom-plugin-example/src/main/resources/static/custom_page_global.js', 'utf8', function (err,data) { - if (err) { - return console.log(err); - } - res.write(data); - res.end(); - }); - } else { - proxy.web(req, res, { target: 'http://127.0.0.1:9000' }); - } -}); -console.log("listening on port 5050") -server.listen(5050); -``` -... and finally run your proxy like this: `node sq-proxy.js` +| SonarQube doesn't guarantee any JavaScript library availability at runtime (except React). If you need a library, include it in the final file. -This will: +## Examples -* run an HTTP proxy on the port 5050 -* catch all calls to the URL /static/example/custom_page_global.js -* serve the file located in the path `/Users/.../sonar-custom-plugin-example/src/main/resources/static/custom_page_global.js` instead of the one available in the Custom Plugin. +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. -- 2.39.5