aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-docs/src/EmbedDocsSuggestions.json53
-rw-r--r--server/sonar-web/config/documentation-loader/fetch-matter.js44
-rw-r--r--server/sonar-web/config/documentation-loader/index.js39
-rw-r--r--server/sonar-web/config/documentation-loader/parse-directory.js24
-rw-r--r--server/sonar-web/config/paths.js4
-rw-r--r--server/sonar-web/config/webpack.config.js16
-rw-r--r--server/sonar-web/package.json3
-rw-r--r--server/sonar-web/scripts/start.js2
-rw-r--r--server/sonar-web/src/main/js/@types/gray-matter.d.ts131
-rw-r--r--server/sonar-web/src/main/js/app/components/NotFound.js17
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json11
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css4
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js2
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Account.js2
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/App.tsx130
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js24
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/routes.ts32
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/utils.ts54
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/system/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocImg.tsx30
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocInclude.tsx70
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocLink.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx20
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocParagraph.tsx35
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocTooltip.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap4
-rw-r--r--server/sonar-web/tsconfig.json6
-rw-r--r--server/sonar-web/yarn.lock55
61 files changed, 952 insertions, 55 deletions
diff --git a/server/sonar-docs/src/EmbedDocsSuggestions.json b/server/sonar-docs/src/EmbedDocsSuggestions.json
new file mode 100644
index 00000000000..7455d98555c
--- /dev/null
+++ b/server/sonar-docs/src/EmbedDocsSuggestions.json
@@ -0,0 +1,53 @@
+{
+ "account": [],
+ "api_documentation": [],
+ "background_tasks": [],
+ "code": [],
+ "coding_rules": [
+ {
+ "link": "/documentation/quality-profiles",
+ "text": "Quality Profiles"
+ }
+ ],
+ "component_measures": [
+ {
+ "link": "/documentation/fixing-the-water-leak",
+ "text": "Fixing the Water Leak"
+ }
+ ],
+ "custom_measures": [],
+ "custom_metrics": [],
+ "global_permissions": [],
+ "issues": [],
+ "marketplace": [],
+ "overview": [
+ {
+ "link": "/documentation/fixing-the-water-leak",
+ "text": "Fixing the Water Leak"
+ }
+ ],
+ "permission_templates": [],
+ "profiles": [],
+ "project_activity": [],
+ "project_quality_gate": [],
+ "project_quality_profiles": [],
+ "projects_management": [],
+ "projects": [],
+ "quality_gates": [
+ {
+ "link": "/documentation/fixing-the-water-leak",
+ "text": "Fixing the Water Leak"
+ }
+ ],
+ "quality_profiles": [
+ {
+ "link": "/documentation/quality-profiles",
+ "text": "Quality Profiles"
+ }
+ ],
+ "settings": [],
+ "system_info": [],
+ "user_groups": [],
+ "users": [],
+ "webhooks": []
+}
diff --git a/server/sonar-web/config/documentation-loader/fetch-matter.js b/server/sonar-web/config/documentation-loader/fetch-matter.js
new file mode 100644
index 00000000000..657a07f08e9
--- /dev/null
+++ b/server/sonar-web/config/documentation-loader/fetch-matter.js
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+const fs = require('fs');
+const path = require('path');
+const matter = require('gray-matter');
+
+const compare = (a, b) => {
+ if (a.order === b.order) return a.title.localeCompare(b.title);
+ if (a.order === -1) return 1;
+ if (b.order === -1) return -1;
+ return a.order - b.order;
+};
+
+module.exports = (root, files) => {
+ return files
+ .map(file => {
+ const content = fs.readFileSync(root + '/' + file, 'utf8');
+ const headerData = matter(content).data;
+ return {
+ name: path.basename(file).slice(0, -3),
+ relativeName: file.slice(0, -3),
+ title: headerData.title || file,
+ order: headerData.order || -1
+ };
+ })
+ .sort(compare);
+};
diff --git a/server/sonar-web/config/documentation-loader/index.js b/server/sonar-web/config/documentation-loader/index.js
new file mode 100644
index 00000000000..d25477bedd9
--- /dev/null
+++ b/server/sonar-web/config/documentation-loader/index.js
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+const path = require('path');
+const parseDirectory = require('./parse-directory');
+const fetchMatter = require('./fetch-matter');
+
+module.exports = function(source) {
+ this.cacheable();
+
+ const failure = this.async();
+ const success = failure.bind(null, null);
+
+ const config = this.exec(source, this.resourcePath);
+ const root = path.resolve(path.dirname(this.resourcePath), config.root);
+ this.addContextDependency(root);
+
+ parseDirectory(root)
+ .then(files => fetchMatter(root, files))
+ .then(result => `module.exports = ${JSON.stringify(result)};`)
+ .then(success)
+ .catch(failure);
+};
diff --git a/server/sonar-web/config/documentation-loader/parse-directory.js b/server/sonar-web/config/documentation-loader/parse-directory.js
new file mode 100644
index 00000000000..35b37040d72
--- /dev/null
+++ b/server/sonar-web/config/documentation-loader/parse-directory.js
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+const glob = require('glob-promise');
+
+module.exports = root => {
+ return glob(root + '/**/*.md').then(files => files.map(file => file.substr(root.length + 1)));
+};
diff --git a/server/sonar-web/config/paths.js b/server/sonar-web/config/paths.js
index 9733ef712ff..d1acbdd2274 100644
--- a/server/sonar-web/config/paths.js
+++ b/server/sonar-web/config/paths.js
@@ -22,5 +22,7 @@ const path = require('path');
module.exports = {
appBuild: path.join(__dirname, '../build/webapp'),
appPublic: path.join(__dirname, '../public'),
- appHtml: path.join(__dirname, '../public/index.html')
+ appHtml: path.join(__dirname, '../public/index.html'),
+ docRoot: path.join(__dirname, '../../sonar-docs/src'),
+ docImages: path.join(__dirname, '../../sonar-docs/src/images')
};
diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js
index cc2bfe7e970..c5439551c5c 100644
--- a/server/sonar-web/config/webpack.config.js
+++ b/server/sonar-web/config/webpack.config.js
@@ -36,7 +36,7 @@ module.exports = ({ production = true }) => ({
extensions: ['.ts', '.tsx', '.js', '.json'],
// import from 'Docs/foo.md' is rewritten to import from 'sonar-docs/src/foo.md'
alias: {
- Docs: path.resolve(__dirname, '../../sonar-docs/src/tooltips')
+ Docs: path.resolve(__dirname, '../../sonar-docs/src')
}
},
entry: [
@@ -79,7 +79,11 @@ module.exports = ({ production = true }) => ({
},
{ test: require.resolve('lodash'), loader: 'expose-loader?_' },
{ test: require.resolve('react'), loader: 'expose-loader?React' },
- { test: require.resolve('react-dom'), loader: 'expose-loader?ReactDOM' }
+ { test: require.resolve('react-dom'), loader: 'expose-loader?ReactDOM' },
+ {
+ test: /\.directory-loader\.js$/,
+ loader: path.resolve(__dirname, 'documentation-loader/index.js')
+ }
].filter(Boolean)
},
plugins: [
@@ -89,6 +93,14 @@ module.exports = ({ production = true }) => ({
production &&
new CopyWebpackPlugin([
{
+ from: paths.docImages,
+ to: paths.appBuild + '/images/embed-doc/images'
+ }
+ ]),
+
+ production &&
+ new CopyWebpackPlugin([
+ {
from: paths.appPublic,
to: paths.appBuild,
ignore: [paths.appHtml]
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index 69537bc59a5..9f300502e18 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -16,6 +16,7 @@
"d3-shape": "1.2.0",
"date-fns": "1.29.0",
"formik": "0.11.7",
+ "gray-matter": "4.0.1",
"history": "3.3.0",
"intl-relativeformat": "2.1.0",
"keymaster": "1.6.2",
@@ -86,6 +87,8 @@
"eslint-plugin-sonarjs": "0.1.0",
"expose-loader": "0.7.5",
"flow-bin": "^0.52.0",
+ "glob": "7.1.2",
+ "glob-promise": "3.4.0",
"html-webpack-plugin": "3.0.6",
"jest": "22.0.6",
"lint-staged": "4.3.0",
diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js
index cef0a40eaab..88fbd9edcff 100644
--- a/server/sonar-web/scripts/start.js
+++ b/server/sonar-web/scripts/start.js
@@ -90,7 +90,7 @@ function runDevServer(compiler, host, port, protocol) {
},
compress: true,
clientLogLevel: 'none',
- contentBase: paths.appPublic,
+ contentBase: [paths.appPublic, paths.docRoot],
disableHostCheck: true,
hot: true,
publicPath: config.output.publicPath,
diff --git a/server/sonar-web/src/main/js/@types/gray-matter.d.ts b/server/sonar-web/src/main/js/@types/gray-matter.d.ts
new file mode 100644
index 00000000000..ee5cb17e576
--- /dev/null
+++ b/server/sonar-web/src/main/js/@types/gray-matter.d.ts
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+/**
+ * Takes a string or object with `content` property, extracts
+ * and parses front-matter from the string, then returns an object
+ * with `data`, `content` and other [useful properties](#returned-object).
+ *
+ * ```js
+ * var matter = require('gray-matter');
+ * console.log(matter('---\ntitle: Home\n---\nOther stuff'));
+ * //=> { data: { title: 'Home'}, content: 'Other stuff' }
+ * ```
+ * @param {Object|String} `input` String, or object with `content` string
+ * @param {Object} `options`
+ * @return {Object}
+ * @api public
+ */
+declare function matter<I extends matter.Input, O extends matter.GrayMatterOption<I, O>>(
+ input: I | { content: I },
+ options?: O
+): matter.GrayMatterFile<I>;
+
+declare namespace matter {
+ type Input = string | Buffer;
+ interface GrayMatterOption<I extends Input, O extends GrayMatterOption<I, O>> {
+ parser?: () => void;
+ // eslint-disable-next-line no-eval
+ eval?: boolean;
+ excerpt?: boolean | ((input: I, options: O) => string);
+ excerpt_separator?: string;
+ engines?: {
+ [index: string]:
+ | ((input: string) => object)
+ | { parse: (input: string) => object; stringify?: (data: object) => string };
+ };
+ language?: string;
+ delimiters?: string | [string, string];
+ }
+ interface GrayMatterFile<I extends Input> {
+ data: { [key: string]: any };
+ content: string;
+ excerpt?: string;
+ orig: Buffer | I;
+ language: string;
+ matter: string;
+ stringify(lang: string): string;
+ }
+
+ /**
+ * Stringify an object to YAML or the specified language, and
+ * append it to the given string. By default, only YAML and JSON
+ * can be stringified. See the [engines](#engines) section to learn
+ * how to stringify other languages.
+ *
+ * ```js
+ * console.log(matter.stringify('foo bar baz', {title: 'Home'}));
+ * // results in:
+ * // ---
+ * // title: Home
+ * // ---
+ * // foo bar baz
+ * ```
+ * @param {String|Object} `file` The content string to append to stringified front-matter, or a file object with `file.content` string.
+ * @param {Object} `data` Front matter to stringify.
+ * @param {Object} `options` [Options](#options) to pass to gray-matter and [js-yaml].
+ * @return {String} Returns a string created by wrapping stringified yaml with delimiters, and appending that to the given string.
+ */
+ export function stringify<O extends GrayMatterOption<string, O>>(
+ file: string | { content: string },
+ data: object,
+ options?: GrayMatterOption<string, O>
+ ): string;
+
+ /**
+ * Synchronously read a file from the file system and parse
+ * front matter. Returns the same object as the [main function](#matter).
+ *
+ * ```js
+ * var file = matter.read('./content/blog-post.md');
+ * ```
+ * @param {String} `filepath` file path of the file to read.
+ * @param {Object} `options` [Options](#options) to pass to gray-matter.
+ * @return {Object} Returns [an object](#returned-object) with `data` and `content`
+ */
+ export function read<O extends GrayMatterOption<string, O>>(
+ fp: string,
+ options?: GrayMatterOption<string, O>
+ ): matter.GrayMatterFile<string>;
+
+ /**
+ * Returns true if the given `string` has front matter.
+ * @param {String} `string`
+ * @param {Object} `options`
+ * @return {Boolean} True if front matter exists.
+ */
+ export function test<O extends matter.GrayMatterOption<string, O>>(
+ str: string,
+ options?: GrayMatterOption<string, O>
+ ): boolean;
+
+ /**
+ * Detect the language to use, if one is defined after the
+ * first front-matter delimiter.
+ * @param {String} `string`
+ * @param {Object} `options`
+ * @return {Object} Object with `raw` (actual language string), and `name`, the language with whitespace trimmed
+ */
+ export function language<O extends matter.GrayMatterOption<string, O>>(
+ str: string,
+ options?: GrayMatterOption<string, O>
+ ): { name: string; raw: string };
+}
+
+export = matter;
diff --git a/server/sonar-web/src/main/js/app/components/NotFound.js b/server/sonar-web/src/main/js/app/components/NotFound.js
index 3f1d81ac0be..37e2a27fa64 100644
--- a/server/sonar-web/src/main/js/app/components/NotFound.js
+++ b/server/sonar-web/src/main/js/app/components/NotFound.js
@@ -21,11 +21,18 @@ import React from 'react';
import { Link } from 'react-router';
import SimpleContainer from './SimpleContainer';
-export default function NotFound() {
+/*::
+type Props = {
+ withContainer?: boolean;
+};
+*/
+
+export default function NotFound({ withContainer = true } /*: Props*/) {
+ const Container = withContainer ? SimpleContainer : React.Fragment;
return (
- <SimpleContainer>
- <div id="bd" className="page-wrapper-simple">
- <div id="nonav" className="page-simple">
+ <Container>
+ <div className="page-wrapper-simple" id="bd">
+ <div className="page-simple" id="nonav">
<h2 className="big-spacer-bottom">The page you were looking for does not exist.</h2>
<p className="spacer-bottom">
You may have mistyped the address or the page may have moved.
@@ -35,6 +42,6 @@ export default function NotFound() {
</p>
</div>
</div>
- </SimpleContainer>
+ </Container>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
index 4a75361d7e5..d81f6bf8806 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
@@ -23,6 +23,7 @@ import { Link } from 'react-router';
import { SuggestionLink } from './SuggestionsProvider';
import BubblePopup, { BubblePopupPosition } from '../../../components/common/BubblePopup';
import { translate } from '../../../helpers/l10n';
+import { getBaseUrl } from '../../../helpers/urls';
interface Props {
onClose: () => void;
@@ -72,7 +73,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
alt={text}
className="spacer-right"
height="18"
- src={'/images/embed-doc/' + icon}
+ src={`${getBaseUrl()}/images/embed-doc/${icon}`}
width="18"
/>
{text}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json
deleted file mode 100644
index ae1ce94f6fd..00000000000
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "projects": [
- { "link": "#", "text": "Foo Suggestion " },
- { "link": "#", "text": "Bar Suggestion " }
- ],
- "profiles": [
- { "link": "#", "text": "Foo Suggestion " },
- { "link": "#", "text": "Bar Suggestion " },
- { "link": "#", "text": "Baz Suggestion " }
- ]
-}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
index 833c382e066..c400a69faba 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
@@ -19,7 +19,8 @@
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
-import * as suggestionsJson from './EmbedDocsSuggestions.json';
+//eslint-disable-next-line import/no-extraneous-dependencies
+import * as suggestionsJson from 'Docs/EmbedDocsSuggestions.json';
import { SuggestionsContext } from './SuggestionsContext';
export interface SuggestionLink {
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index 80c0438930e..bcdeb28cd2f 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -189,6 +189,10 @@ td.big-spacer-top {
overflow: hidden;
}
+.max-width-100 {
+ max-width: 100%;
+}
+
.width-100 {
width: 100%;
}
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index 6d38859ecd7..35ba6778ddf 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -75,6 +75,7 @@ import settingsRoutes from '../../apps/settings/routes';
import systemRoutes from '../../apps/system/routes';
import usersRoutes from '../../apps/users/routes';
import webAPIRoutes from '../../apps/web-api/routes';
+import documentationRoutes from '../../apps/documentation/routes';
import webhooksRoutes from '../../apps/webhooks/routes';
import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes';
import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
@@ -170,6 +171,7 @@ const startReactApp = () => {
<Route path="account" childRoutes={accountRoutes} />
<Route path="coding_rules" childRoutes={codingRulesRoutes} />
<Route path="component" childRoutes={componentRoutes} />
+ <Route path="documentation" childRoutes={documentationRoutes} />
<Route path="explore" component={Explore}>
<Route path="issues" component={ExploreIssues} />
<Route path="projects" component={ExploreProjects} />
diff --git a/server/sonar-web/src/main/js/apps/account/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js
index 596c837c096..0266a3efaf6 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Account.js
+++ b/server/sonar-web/src/main/js/apps/account/components/Account.js
@@ -25,6 +25,7 @@ import UserCard from './UserCard';
import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import '../account.css';
class Account extends React.PureComponent {
@@ -44,6 +45,7 @@ class Account extends React.PureComponent {
const title = translate('my_account.page');
return (
<div id="account-page">
+ <Suggestions suggestions="account" />
<Helmet defaultTitle={title} titleTemplate={'%s - ' + title} />
<header className="account-header">
<div className="account-container clearfix">
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
index 0c2770ca719..b8d27cf0e7f 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
@@ -29,6 +29,7 @@ import Footer from './Footer';
import StatsContainer from '../components/StatsContainer';
import Search from '../components/Search';
import Tasks from '../components/Tasks';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
getTypes,
getActivity,
@@ -227,6 +228,7 @@ class BackgroundTasksApp extends React.PureComponent {
return (
<div className="page page-limited">
+ <Suggestions suggestions="background_tasks" />
<Helmet title={translate('background_tasks.page')} />
<Header component={component} />
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx
index 3c9ebfc25f4..99f607b359f 100644
--- a/server/sonar-web/src/main/js/apps/code/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx
@@ -29,6 +29,7 @@ import { retrieveComponentChildren, retrieveComponent, loadMoreChildren } from '
import { Component, BranchLike } from '../../../app/types';
import ListFooter from '../../../components/controls/ListFooter';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { isSameBranchLike } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
import { parseError } from '../../../helpers/request';
@@ -198,6 +199,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<div className="page page-limited">
+ <Suggestions suggestions="code" />
<Helmet title={sourceViewer !== undefined ? sourceViewer.name : translate('code.page')} />
{error && <div className="alert alert-danger">{error}</div>}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
index 19441b29218..f9ad0c2eafd 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
@@ -27,6 +27,7 @@ import FacetsList from './FacetsList';
import PageActions from './PageActions';
import RuleDetails from './RuleDetails';
import RuleListItem from './RuleListItem';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
Facets,
Query,
@@ -466,6 +467,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<>
+ <Suggestions suggestions="coding_rules" />
<Helmet title={translate('coding_rules.page')}>
<meta content="noindex" name="robots" />
</Helmet>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
index b18f85ca24f..03e0dd641ec 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
@@ -27,6 +27,7 @@ import Sidebar from '../sidebar/Sidebar';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery } from '../utils';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
getLocalizedMetricDomain,
translateWithParameters,
@@ -177,6 +178,7 @@ export default class App extends React.PureComponent {
const metric = metrics[query.metric];
return (
<div className="layout-page" id="component-measures">
+ <Suggestions suggestions="component_measures" />
<Helmet title={this.getHelmetTitle(metric, query)} />
<ScreenPositionHelper className="layout-page-side-outer">
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
index a5a8537c675..70583de970f 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
className="layout-page"
id="component-measures"
>
+ <Suggestions
+ suggestions="component_measures"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
index 79e4a5e5d67..620b3f6a528 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
@@ -28,6 +28,7 @@ import {
deleteCustomMeasure
} from '../../../api/measures';
import { Paging, CustomMeasure } from '../../../app/types';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import ListFooter from '../../../components/controls/ListFooter';
import { translate } from '../../../helpers/l10n';
@@ -133,6 +134,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<>
+ <Suggestions suggestions="custom_measures" />
<Helmet title={translate('custom_measures.page')} />
<div className="page page-limited">
<Header
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap
index cb97413281e..c9a08f12b4d 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -2,6 +2,9 @@
exports[`should work 1`] = `
<React.Fragment>
+ <Suggestions
+ suggestions="custom_measures"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -20,6 +23,9 @@ exports[`should work 1`] = `
exports[`should work 2`] = `
<React.Fragment>
+ <Suggestions
+ suggestions="custom_measures"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx
index f802bc704f8..a8f87cf9941 100644
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx
@@ -22,6 +22,7 @@ import { Helmet } from 'react-helmet';
import { MetricProps } from './Form';
import Header from './Header';
import List from './List';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
getMetricDomains,
getMetricTypes,
@@ -146,6 +147,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<>
+ <Suggestions suggestions="custom_metrics" />
<Helmet title={translate('custom_metrics.page')} />
<div className="page page-limited" id="custom-metrics-page">
<Header domains={domains} loading={loading} onCreate={this.handleCreate} types={types} />
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap
index 55f852e2689..6a3320cd191 100644
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -2,6 +2,9 @@
exports[`should work 1`] = `
<React.Fragment>
+ <Suggestions
+ suggestions="custom_metrics"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -21,6 +24,9 @@ exports[`should work 1`] = `
exports[`should work 2`] = `
<React.Fragment>
+ <Suggestions
+ suggestions="custom_metrics"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx
new file mode 100644
index 00000000000..90b0bfb46ea
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import * as matter from 'gray-matter';
+import Helmet from 'react-helmet';
+import { Link } from 'react-router';
+import Menu from './Menu';
+import NotFound from '../../../app/components/NotFound';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ params: { splat?: string };
+}
+
+interface State {
+ content?: string;
+ loading: boolean;
+ notFound: boolean;
+}
+
+export default class App extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: false, notFound: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchContent(this.props.params.splat || 'index');
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ const newSplat = nextProps.params.splat || 'index';
+ if (newSplat !== this.props.params.splat) {
+ this.setState({ content: undefined });
+ this.fetchContent(newSplat);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchContent = (path: string) => {
+ this.setState({ loading: true });
+ import(`Docs/pages/${path === '' ? 'index' : path}.md`).then(
+ ({ default: content }) => {
+ if (this.mounted) {
+ this.setState({ content, loading: false, notFound: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false, notFound: true });
+ }
+ }
+ );
+ };
+
+ renderContent() {
+ if (this.state.loading) {
+ return <DeferredSpinner />;
+ }
+
+ if (this.state.notFound) {
+ return <NotFound withContainer={false} />;
+ }
+
+ return (
+ <div className="boxed-group">
+ <DocMarkdownBlock
+ className="cut-margins boxed-group-inner"
+ content={this.state.content}
+ displayH1={true}
+ />
+ </div>
+ );
+ }
+
+ render() {
+ const pageTitle = matter(this.state.content || '').data.title;
+ const mainTitle = translate('documentation.page');
+ const isIndex = !this.props.params.splat || this.props.params.splat === '';
+ return (
+ <div className="layout-page">
+ <Helmet title={isIndex || this.state.notFound ? mainTitle : `${pageTitle} - ${mainTitle}`}>
+ <meta content="noindex nofollow" name="robots" />
+ </Helmet>
+ <ScreenPositionHelper className="layout-page-side-outer">
+ {({ top }) => (
+ <div className="layout-page-side" style={{ top }}>
+ <div className="layout-page-side-inner">
+ <div className="layout-page-filters">
+ <div className="web-api-page-header">
+ <Link to="/documentation/">
+ <h1>{translate('documentation.page')}</h1>
+ </Link>
+ </div>
+ <Menu splat={this.props.params.splat} />
+ </div>
+ </div>
+ </div>
+ )}
+ </ScreenPositionHelper>
+
+ <div className="layout-page-main">
+ <div className="layout-page-main-inner">{this.renderContent()}</div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
new file mode 100644
index 00000000000..6eb6a4c610d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import { Link } from 'react-router';
+import * as classNames from 'classnames';
+import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';
+import {
+ activeOrChildrenActive,
+ DocumentationEntry,
+ getEntryChildren,
+ getEntryRoot
+} from '../utils';
+import * as Docs from '../documentation.directory-loader';
+
+interface Props {
+ splat?: string;
+}
+
+export default class Menu extends React.PureComponent<Props> {
+ getMenuEntriesHierarchy = (root?: string): Array<DocumentationEntry> => {
+ const toplevelEntries = getEntryChildren(Docs as any, root);
+ toplevelEntries.forEach(entry => {
+ const entryRoot = getEntryRoot(entry.relativeName);
+ entry.children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
+ });
+ return toplevelEntries;
+ };
+
+ renderEntry = (entry: DocumentationEntry, depth: number): React.ReactNode => {
+ const active = entry.relativeName === this.props.splat;
+ const opened = activeOrChildrenActive(this.props.splat || '', entry);
+ const offset = 10 + 25 * depth;
+ return (
+ <React.Fragment key={entry.name}>
+ <Link
+ className={classNames('list-group-item', { active })}
+ style={{ paddingLeft: offset }}
+ to={'/documentation/' + entry.relativeName}>
+ <h3 className="list-group-item-heading">
+ {entry.children.length > 0 && (
+ <OpenCloseIcon className="little-spacer-right" open={opened} />
+ )}
+ {entry.title}
+ </h3>
+ </Link>
+ {opened && entry.children.map(entry => this.renderEntry(entry, depth + 1))}
+ </React.Fragment>
+ );
+ };
+
+ render() {
+ return (
+ <div className="api-documentation-results panel">
+ <div className="list-group">
+ {this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js b/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js
new file mode 100644
index 00000000000..447060a8f47
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+const path = require('path');
+
+module.exports = {
+ root: path.resolve(__dirname, '../../../../../../sonar-docs/src/pages')
+};
diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.ts b/server/sonar-web/src/main/js/apps/documentation/routes.ts
new file mode 100644
index 00000000000..704e4739073
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/routes.ts
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { lazyLoad } from '../../components/lazyLoad';
+
+const routes = [
+ {
+ indexRoute: { component: lazyLoad(() => import('./components/App')) }
+ },
+ {
+ path: '**',
+ indexRoute: { component: lazyLoad(() => import('./components/App')) }
+ }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/documentation/utils.ts b/server/sonar-web/src/main/js/apps/documentation/utils.ts
new file mode 100644
index 00000000000..70ec0d1f12b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/utils.ts
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 DocumentationEntry {
+ title: string;
+ order: string;
+ name: string;
+ relativeName: string;
+ children: Array<DocumentationEntry>;
+}
+
+export function activeOrChildrenActive(root: string, entry: DocumentationEntry) {
+ return root.indexOf(getEntryRoot(entry.relativeName)) === 0;
+}
+
+export function getEntryRoot(name: string) {
+ if (name.endsWith('index')) {
+ return name
+ .split('/')
+ .slice(0, -1)
+ .join('/');
+ }
+ return name;
+}
+
+export function getEntryChildren(
+ entries: Array<DocumentationEntry>,
+ root?: string
+): Array<DocumentationEntry> {
+ return entries.filter(entry => {
+ const parts = entry.relativeName.split('/');
+ const depth = root ? root.split('/').length : 0;
+ return (
+ (!root || entry.relativeName.indexOf(root) === 0) &&
+ ((parts.length === depth + 1 && parts[depth] !== 'index') || parts[depth + 1] === 'index')
+ );
+ });
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/App.tsx b/server/sonar-web/src/main/js/apps/groups/components/App.tsx
index dac3048bf49..e44998e4bde 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/App.tsx
@@ -21,6 +21,7 @@ import * as React from 'react';
import { Helmet } from 'react-helmet';
import Header from './Header';
import List from './List';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { searchUsersGroups, deleteGroup, updateGroup, createGroup } from '../../../api/user_groups';
import { Group, Paging } from '../../../app/types';
import ListFooter from '../../../components/controls/ListFooter';
@@ -140,6 +141,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<>
+ <Suggestions suggestions="user_groups" />
<Helmet title={translate('user_groups.page')} />
<div className="page page-limited" id="groups-page">
<Header loading={loading} onCreate={this.handleCreate} />
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index 3ab8f9023e8..df3a3a1374c 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -34,6 +34,7 @@ import PageActions from './PageActions';
import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList';
import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader';
import Sidebar from '../sidebar/Sidebar';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import * as actions from '../actions';
import {
areMyIssuesSelected,
@@ -1003,6 +1004,7 @@ export default class App extends React.PureComponent<Props, State> {
const selectedIndex = this.getSelectedIndex();
return (
<div className="layout-page issues" id="issues-page">
+ <Suggestions suggestions="issues" />
<Helmet title={openIssue ? openIssue.message : translate('issues.page')} />
{this.renderSide(openIssue)}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx
index 9446bfe471c..8b151058a49 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx
@@ -27,6 +27,7 @@ import Footer from './Footer';
import PluginsList from './PluginsList';
import Search from './Search';
import { filterPlugins, parseQuery, Query, serializeQuery } from './utils';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import {
getAvailablePlugins,
getInstalledPluginsWithUpdates,
@@ -136,6 +137,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<div className="page page-limited" id="marketplace-page">
+ <Suggestions suggestions="marketplace" />
<Helmet title={translate('marketplace.page')} />
<Header />
<EditionBoxes
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
index ff2bff3aa8c..097e642860f 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
@@ -29,6 +29,7 @@ import Coverage from '../main/Coverage';
import Duplications from '../main/Duplications';
import Meta from '../meta/Meta';
import throwGlobalError from '../../../app/utils/throwGlobalError';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { getMeasuresAndMeta } from '../../../api/measures';
import { getAllTimeMachineData, History } from '../../../api/time-machine';
import { parseDate } from '../../../helpers/dates';
@@ -237,6 +238,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
return (
<div className="page page-limited">
<div className="overview page-with-sidebar">
+ <Suggestions suggestions="overview" />
<Helmet>
<link href={getPathUrlAsString(getProjectUrl(component.key))} rel="canonical" />
</Helmet>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/App.js b/server/sonar-web/src/main/js/apps/permission-templates/components/App.js
index 33e8bea47d0..df020fbe8a4 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/App.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/App.js
@@ -22,6 +22,7 @@ import PropTypes from 'prop-types';
import Home from './Home';
import Template from './Template';
import OrganizationHelmet from '../../../components/common/OrganizationHelmet';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { getPermissionTemplates } from '../../../api/permissions';
import { sortPermissions, mergePermissionsToTemplates, mergeDefaultsToTemplates } from '../utils';
import { translate } from '../../../helpers/l10n';
@@ -103,6 +104,7 @@ export default class App extends React.PureComponent {
const { id } = this.props.location.query;
return (
<div>
+ <Suggestions suggestions="permission_templates" />
<OrganizationHelmet
title={translate('permission_templates.page')}
organization={this.props.organization}
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
index ed4397179b8..d79c2bd51c8 100644
--- a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
@@ -22,6 +22,7 @@ import Helmet from 'react-helmet';
import PageHeader from './PageHeader';
import AllHoldersListContainer from './AllHoldersListContainer';
import PageError from '../../shared/components/PageError';
+import Suggestions from '../../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../../helpers/l10n';
import { Organization } from '../../../../app/types';
import '../../styles.css';
@@ -33,6 +34,7 @@ interface Props {
export default function App({ organization }: Props) {
return (
<div className="page page-limited">
+ <Suggestions suggestions="global_permissions" />
<Helmet title={translate('global_permissions.permission')} />
<PageHeader organization={organization} />
<PageError />
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
index cc920d30286..567853ebe55 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
@@ -25,6 +25,7 @@ import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
import ProjectActivityGraphs from './ProjectActivityGraphs';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import './projectActivity.css';
/*:: import type { Analysis, MeasureHistory, Metric, Query } from '../types'; */
@@ -61,6 +62,7 @@ export default function ProjectActivityApp(props /*: Props */) {
const canDeleteAnalyses = configuration ? configuration.showHistory : false;
return (
<div id="project-activity" className="page page-limited">
+ <Suggestions suggestions="project_activity" />
<Helmet title={translate('project_activity.page')} />
<ProjectActivityPageHeader
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
index 71e18479081..1500e14c91d 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
className="page page-limited"
id="project-activity"
>
+ <Suggestions
+ suggestions="project_activity"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
index 4b6c1615d89..9d1d86e32dc 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
@@ -28,6 +28,7 @@ import {
dissociateGateWithProject,
QualityGate
} from '../../api/quality-gates';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
import { Component } from '../../app/types';
@@ -128,6 +129,7 @@ export default class App extends React.PureComponent<Props> {
return (
<div id="project-quality-gate" className="page page-limited">
+ <Suggestions suggestions="project_quality_gate" />
<Helmet title={translate('project_quality_gate.page')} />
<Header />
{loading ? (
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
index ff0be985150..158e002447c 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
@@ -34,7 +34,7 @@ jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
}));
import * as React from 'react';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
import App from '../App';
import { Component } from '../../../app/types';
@@ -73,7 +73,7 @@ beforeEach(() => {
it('checks permissions', () => {
handleRequiredAuthorization.mockClear();
- mount(
+ shallow(
<App
component={{ ...component, configuration: undefined } as Component}
onComponentChange={jest.fn()}
@@ -85,7 +85,7 @@ it('checks permissions', () => {
it('fetches quality gates', () => {
fetchQualityGates.mockClear();
getGateForProject.mockClear();
- mount(<App component={component} onComponentChange={jest.fn()} />);
+ shallow(<App component={component} onComponentChange={jest.fn()} />);
expect(fetchQualityGates).toBeCalledWith({ organization: 'org' });
expect(getGateForProject).toBeCalledWith({ organization: 'org', project: 'component' });
});
@@ -140,7 +140,7 @@ function randomGate(id: string, isDefault = false) {
}
function mountRender(allGates: any[], gate?: any) {
- const wrapper = mount(<App component={component} onComponentChange={jest.fn()} />);
+ const wrapper = shallow(<App component={component} onComponentChange={jest.fn()} />);
wrapper.setState({ allGates, loading: false, gate });
return wrapper;
}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
index a1f5ed8163a..381e56d4fd1 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
@@ -28,6 +28,7 @@ import {
Profile
} from '../../api/quality-profiles';
import { Component } from '../../app/types';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
import { translate, translateWithParameters } from '../../helpers/l10n';
@@ -118,6 +119,7 @@ export default class QualityProfiles extends React.PureComponent<Props, State> {
return (
<div className="page page-limited">
+ <Suggestions suggestions="project_quality_profiles" />
<Helmet title={translate('project_quality_profiles.page')} />
<Header />
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
index 42756844899..685ceb6f749 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
@@ -33,7 +33,7 @@ jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
}));
import * as React from 'react';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
import App from '../App';
const associateProject = require('../../../api/quality-profiles').associateProject as jest.Mock<
@@ -66,13 +66,13 @@ const component = {
it('checks permissions', () => {
handleRequiredAuthorization.mockClear();
- mount(<App component={{ ...component, configuration: undefined }} />);
+ shallow(<App component={{ ...component, configuration: undefined }} />);
expect(handleRequiredAuthorization).toBeCalled();
});
it('fetches profiles', () => {
searchQualityProfiles.mockClear();
- mount(<App component={component} />);
+ shallow(<App component={component} />);
expect(searchQualityProfiles.mock.calls).toHaveLength(2);
expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' });
expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', project: 'foo' });
@@ -82,7 +82,7 @@ it('changes profile', () => {
associateProject.mockClear();
dissociateProject.mockClear();
addGlobalSuccessMessage.mockClear();
- const wrapper = mount(<App component={component} />);
+ const wrapper = shallow(<App component={component} />);
const fooJava = randomProfile('foo-java', 'java');
const fooJs = randomProfile('foo-js', 'js');
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
index 24b422552fe..978feca7bba 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
@@ -26,6 +26,7 @@ import Projects from './Projects';
import CreateProjectForm from './CreateProjectForm';
import { PAGE_SIZE, Project } from './utils';
import ListFooter from '../../components/controls/ListFooter';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getComponents } from '../../api/components';
import { Organization } from '../../app/types';
import { toNotSoISOString } from '../../helpers/dates';
@@ -164,6 +165,7 @@ export default class App extends React.PureComponent<Props, State> {
render() {
return (
<div className="page page-limited" id="projects-management-page">
+ <Suggestions suggestions="projects_management" />
<Helmet title={translate('projects_management')} />
<Header
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
index 81e1034c736..dbd4a441f63 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
@@ -23,6 +23,7 @@ import Helmet from 'react-helmet';
import ListHeader from './ListHeader';
import List from './List';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { fetchQualityGates } from '../../../api/quality-gates';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
@@ -79,6 +80,7 @@ export default class QualityGatesApp extends Component {
const defaultTitle = translate('quality_gates.page');
return (
<div id="quality-gates-page" className="layout-page">
+ <Suggestions suggestions="quality_gates" />
<Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
<ScreenPositionHelper className="layout-page-side-outer">
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
index a831d023512..2fe1b621c97 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import { searchQualityProfiles, getExporters, Actions } from '../../../api/quality-profiles';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { sortProfiles } from '../utils';
import { translate } from '../../../helpers/l10n';
import OrganizationHelmet from '../../../components/common/OrganizationHelmet';
@@ -109,6 +110,7 @@ export default class App extends React.PureComponent<Props, State> {
render() {
return (
<div className="page page-limited">
+ <Suggestions suggestions="quality_profiles" />
<OrganizationHelmet
title={translate('quality_profiles.page')}
organization={this.props.organization}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js
index 00e3599079b..ad96321df75 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/App.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/App.js
@@ -24,6 +24,7 @@ import PageHeader from './PageHeader';
import CategoryDefinitionsList from './CategoryDefinitionsList';
import AllCategoriesList from './AllCategoriesList';
import WildcardsHelp from './WildcardsHelp';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import '../styles.css';
@@ -68,6 +69,7 @@ export default class App extends React.PureComponent {
return (
<div id="settings-page" className="page page-limited">
+ <Suggestions suggestions="settings" />
<Helmet title={translate('settings.page')} />
<PageHeader component={this.props.component} />
diff --git a/server/sonar-web/src/main/js/apps/system/components/App.tsx b/server/sonar-web/src/main/js/apps/system/components/App.tsx
index 07ce3c1d325..2d4b850ae93 100644
--- a/server/sonar-web/src/main/js/apps/system/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/system/components/App.tsx
@@ -24,6 +24,7 @@ import ClusterSysInfos from './ClusterSysInfos';
import PageHeader from './PageHeader';
import StandaloneSysInfos from './StandaloneSysInfos';
import SystemUpgradeNotif from './system-upgrade/SystemUpgradeNotif';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import { ClusterSysInfo, getSystemInfo, SysInfo } from '../../../api/system';
import {
@@ -128,6 +129,7 @@ export default class App extends React.PureComponent<Props, State> {
const { loading, sysInfoData } = this.state;
return (
<div className="page page-limited">
+ <Suggestions suggestions="system_info" />
<Helmet title={translate('system_info.page')} />
<SystemUpgradeNotif />
<PageHeader
diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
index 1cf4ee7e669..e0ea3203655 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
@@ -26,6 +26,7 @@ import Search from './Search';
import UsersList from './UsersList';
import { parseQuery, Query, serializeQuery } from './utils';
import ListFooter from '../../components/controls/ListFooter';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getIdentityProviders, searchUsers } from '../../api/users';
import { Paging, IdentityProvider, User } from '../../app/types';
import { translate } from '../../helpers/l10n';
@@ -124,6 +125,7 @@ export default class UsersApp extends React.PureComponent<Props, State> {
const { loading, paging, users } = this.state;
return (
<div id="users-page" className="page page-limited">
+ <Suggestions suggestions="users" />
<Helmet title={translate('users.page')} />
<Header loading={loading} onUpdateUsers={this.fetchUsers} />
<Search query={query} updateQuery={this.updateQuery} />
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
index c955a2eef06..68ea461d488 100644
--- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
className="page page-limited"
id="users-page"
>
+ <Suggestions
+ suggestions="users"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -43,6 +46,9 @@ exports[`should render correctly 2`] = `
className="page page-limited"
id="users-page"
>
+ <Suggestions
+ suggestions="users"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
index cb2b5e5f4d2..0b79a15c483 100644
--- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
+++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
@@ -29,6 +29,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe
import { getActionKey, isDomainPathActive } from '../utils';
import { scrollToElement } from '../../../helpers/scrolling';
import { translate } from '../../../helpers/l10n';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import '../styles/web-api.css';
interface Props {
@@ -148,6 +149,7 @@ export default class WebApiApp extends React.PureComponent<Props, State> {
return (
<div className="layout-page">
+ <Suggestions suggestions="api_documentation" />
<Helmet title={translate('api_documentation.page')} />
<ScreenPositionHelper className="layout-page-side-outer">
{({ top }) => (
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
index 9d33f1486e9..e29280e7d14 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
@@ -23,6 +23,7 @@ import PageActions from './PageActions';
import PageHeader from './PageHeader';
import WebhooksList from './WebhooksList';
import { createWebhook, deleteWebhook, searchWebhooks, updateWebhook } from '../../../api/webhooks';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { LightComponent, Organization, Webhook } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
@@ -113,6 +114,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<>
+ <Suggestions suggestions="webhooks" />
<Helmet title={translate('webhooks.page')} />
<div className="page page-limited">
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
index 43c7785c6e0..e8934607b3e 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -2,6 +2,9 @@
exports[`should be in loading status 1`] = `
<React.Fragment>
+ <Suggestions
+ suggestions="webhooks"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -25,6 +28,9 @@ exports[`should be in loading status 1`] = `
exports[`should fetch webhooks and display them 1`] = `
<React.Fragment>
+ <Suggestions
+ suggestions="webhooks"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/components/docs/DocImg.tsx b/server/sonar-web/src/main/js/components/docs/DocImg.tsx
new file mode 100644
index 00000000000..4df9753ccb1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocImg.tsx
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+
+export default function DocImg(props: React.ImgHTMLAttributes<HTMLImageElement>) {
+ const { alt, src, ...other } = props;
+
+ if (process.env.NODE_ENV === 'development') {
+ return <img alt={alt} className="max-width-100" src={'/' + src} {...other} />;
+ }
+
+ return <img alt={alt} className="max-width-100" src={'/images/embed-doc/' + src} {...other} />;
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocInclude.tsx b/server/sonar-web/src/main/js/components/docs/DocInclude.tsx
new file mode 100644
index 00000000000..b74eff2421c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocInclude.tsx
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import DocMarkdownBlock from './DocMarkdownBlock';
+
+interface Props {
+ path: string;
+}
+
+interface State {
+ content?: string;
+}
+
+export default class DocInclude extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {};
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchContent();
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.path !== this.props.path) {
+ this.setState({ content: undefined });
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.path !== this.props.path) {
+ this.fetchContent();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchContent = () => {
+ import(`Docs/${this.props.path}.md`).then(
+ ({ default: content }) => {
+ if (this.mounted) {
+ this.setState({ content });
+ }
+ },
+ () => {}
+ );
+ };
+
+ render() {
+ return <DocMarkdownBlock content={this.state.content} />;
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocLink.tsx b/server/sonar-web/src/main/js/components/docs/DocLink.tsx
index fd19f91cd94..17b99af9c4e 100644
--- a/server/sonar-web/src/main/js/components/docs/DocLink.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocLink.tsx
@@ -23,18 +23,12 @@ import { Link } from 'react-router';
export default function DocLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const { children, href, ...other } = props;
- if (process.env.NODE_ENV === 'development') {
- if (href && href.startsWith('#')) {
- return (
- <>
- {/* TODO implement after SONAR-10612 Create documentation space in the web app */}
- <Link to="" {...other}>
- {children}
- </Link>
- <strong className="little-spacer-left text-danger">[TODO]</strong>
- </>
- );
- }
+ if (href && href.startsWith('/')) {
+ return (
+ <Link to={`/documentation/${href.substr(1)}`} {...other}>
+ {children}
+ </Link>
+ );
}
return (
diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
index 7a74a9f4b10..88fcfb65cc8 100644
--- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
@@ -21,27 +21,39 @@ import * as React from 'react';
import * as classNames from 'classnames';
import remark from 'remark';
import reactRenderer from 'remark-react';
+import * as matter from 'gray-matter';
import DocLink from './DocLink';
+import DocParagraph from './DocParagraph';
+import DocImg from './DocImg';
interface Props {
className?: string;
content: string | undefined;
+ displayH1?: boolean;
}
-export default function DocMarkdownBlock({ className, content }: Props) {
+export default function DocMarkdownBlock({ className, content, displayH1 }: Props) {
+ const parsed = matter(content || '');
return (
<div className={classNames('markdown', className)}>
+ {displayH1 && <h1>{parsed.data.title}</h1>}
{
remark()
+ // .use(remarkInclude)
.use(reactRenderer, {
remarkReactComponents: {
// do not render outer <div />
div: React.Fragment,
// use custom link to render documentation anchors
- a: DocLink
- }
+ a: DocLink,
+ // used to handle `@include`
+ p: DocParagraph,
+ // use custom img tag to render documentation images
+ img: DocImg
+ },
+ toHast: {}
})
- .processSync(content).contents
+ .processSync(parsed.content).contents
}
</div>
);
diff --git a/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx b/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx
new file mode 100644
index 00000000000..54b34660482
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import DocInclude from './DocInclude';
+
+const INCLUDE = '@include';
+
+export default function DocParagraph(props: React.HTMLAttributes<HTMLParagraphElement>) {
+ if (Array.isArray(props.children) && props.children.length === 1) {
+ const child = props.children[0];
+ if (typeof child === 'string' && child.startsWith(INCLUDE)) {
+ const includePath = child.substr(INCLUDE.length + 1);
+ return <DocInclude path={includePath} />;
+ }
+ }
+
+ return <p {...props} />;
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
index 0dc27dd9954..5c719443490 100644
--- a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
@@ -59,7 +59,7 @@ export default class DocTooltip extends React.PureComponent<Props, State> {
fetchContent = () => {
this.setState({ loading: true });
- import(`Docs/${this.props.doc}.md`).then(
+ import(`Docs/tooltips/${this.props.doc}.md`).then(
({ default: content }) => {
if (this.mounted) {
this.setState({ content, loading: false });
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
index 28a2441e530..7d44e66dc15 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
@@ -7,7 +7,7 @@ exports[`should render simple markdown 1`] = `
<React.Fragment
key="h-1"
>
- <p
+ <DocParagraph
key="h-2"
>
this is
@@ -17,7 +17,7 @@ exports[`should render simple markdown 1`] = `
bold
</em>
text
- </p>
+ </DocParagraph>
</React.Fragment>
</div>
`;
diff --git a/server/sonar-web/tsconfig.json b/server/sonar-web/tsconfig.json
index 4e877399ac5..11eaced5e1e 100644
--- a/server/sonar-web/tsconfig.json
+++ b/server/sonar-web/tsconfig.json
@@ -12,7 +12,11 @@
"lib": ["es2017", "dom"],
"module": "esnext",
"moduleResolution": "node",
- "sourceMap": true
+ "sourceMap": true,
+ "baseUrl": ".",
+ "paths": {
+ "*": ["./src/main/js/@types/*"]
+ }
},
"include": ["./src/main/js/**/*"]
}
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 7bbd7883247..3707c3c7e8c 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -43,6 +43,18 @@
"@types/cheerio" "*"
"@types/react" "*"
+"@types/events@*":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
+
+"@types/glob@*":
+ version "5.0.35"
+ resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a"
+ dependencies:
+ "@types/events" "*"
+ "@types/minimatch" "*"
+ "@types/node" "*"
+
"@types/history@^3":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.2.tgz#b6affa240cb10b5f841c6443d8a24d7f3fc8bb0c"
@@ -59,6 +71,10 @@
version "4.14.102"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.102.tgz#586a3e22385fc79b07cef9c5a1c8a5387986fbc8"
+"@types/minimatch@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+
"@types/node@*":
version "6.0.90"
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.90.tgz#0ed74833fa1b73dcdb9409dcb1c97ec0a8b13b02"
@@ -3506,13 +3522,19 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
+glob-promise@3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20"
+ dependencies:
+ "@types/glob" "*"
+
glob2base@^0.0.12:
version "0.0.12"
resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56"
dependencies:
find-index "^0.1.1"
-glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+glob@7.1.2, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3591,6 +3613,15 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+gray-matter@4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.1.tgz#375263c194f0d9755578c277e41b1c1dfdf22c7d"
+ dependencies:
+ js-yaml "^3.11.0"
+ kind-of "^6.0.2"
+ section-matter "^1.0.0"
+ strip-bom-string "^1.0.0"
+
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -4774,6 +4805,13 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+js-yaml@^3.11.0:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1:
version "3.10.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
@@ -5122,10 +5160,6 @@ lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
-lodash.cond@^4.3.0:
- version "4.5.2"
- resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
-
lodash.escape@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698"
@@ -7463,6 +7497,13 @@ schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.5:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
+section-matter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
+ dependencies:
+ extend-shallow "^2.0.1"
+ kind-of "^6.0.0"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7959,6 +8000,10 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
+strip-bom-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
+
strip-bom@3.0.0, strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"