From b083d4376580b8dd252933025ca86ce5b98ce7af Mon Sep 17 00:00:00 2001 From: Pascal Mugnier Date: Thu, 26 Apr 2018 08:42:19 +0200 Subject: [PATCH] SONAR-10612 Create documentation space in the web app --- .../sonar-docs/src/EmbedDocsSuggestions.json | 53 +++++++ .../documentation-loader/fetch-matter.js | 44 ++++++ .../config/documentation-loader/index.js | 39 ++++++ .../documentation-loader/parse-directory.js | 24 ++++ server/sonar-web/config/paths.js | 4 +- server/sonar-web/config/webpack.config.js | 16 ++- server/sonar-web/package.json | 3 + server/sonar-web/scripts/start.js | 2 +- .../src/main/js/@types/gray-matter.d.ts | 131 ++++++++++++++++++ .../src/main/js/app/components/NotFound.js | 17 ++- .../embed-docs-modal/EmbedDocsPopup.tsx | 3 +- .../EmbedDocsSuggestions.json | 11 -- .../embed-docs-modal/SuggestionsProvider.tsx | 3 +- .../src/main/js/app/styles/init/misc.css | 4 + .../src/main/js/app/utils/startReactApp.js | 2 + .../js/apps/account/components/Account.js | 2 + .../components/BackgroundTasksApp.js | 2 + .../src/main/js/apps/code/components/App.tsx | 2 + .../js/apps/coding-rules/components/App.tsx | 2 + .../apps/component-measures/components/App.js | 2 + .../__tests__/__snapshots__/App-test.js.snap | 3 + .../apps/custom-measures/components/App.tsx | 2 + .../__tests__/__snapshots__/App-test.tsx.snap | 6 + .../js/apps/custom-metrics/components/App.tsx | 2 + .../__tests__/__snapshots__/App-test.tsx.snap | 6 + .../js/apps/documentation/components/App.tsx | 130 +++++++++++++++++ .../js/apps/documentation/components/Menu.tsx | 77 ++++++++++ .../documentation.directory-loader.js | 24 ++++ .../src/main/js/apps/documentation/routes.ts | 32 +++++ .../src/main/js/apps/documentation/utils.ts | 54 ++++++++ .../main/js/apps/groups/components/App.tsx | 2 + .../main/js/apps/issues/components/App.tsx | 2 + .../src/main/js/apps/marketplace/App.tsx | 2 + .../apps/overview/components/OverviewApp.tsx | 2 + .../permission-templates/components/App.js | 2 + .../permissions/global/components/App.tsx | 2 + .../components/ProjectActivityApp.js | 2 + .../ProjectActivityApp-test.js.snap | 3 + .../main/js/apps/projectQualityGate/App.tsx | 2 + .../projectQualityGate/__tests__/App-test.tsx | 8 +- .../js/apps/projectQualityProfiles/App.tsx | 2 + .../__tests__/App-test.tsx | 8 +- .../main/js/apps/projectsManagement/App.tsx | 2 + .../components/QualityGatesApp.js | 2 + .../apps/quality-profiles/components/App.tsx | 2 + .../main/js/apps/settings/components/App.js | 2 + .../main/js/apps/system/components/App.tsx | 2 + .../src/main/js/apps/users/UsersApp.tsx | 2 + .../__snapshots__/UsersApp-test.tsx.snap | 6 + .../js/apps/web-api/components/WebApiApp.tsx | 2 + .../main/js/apps/webhooks/components/App.tsx | 2 + .../__tests__/__snapshots__/App-test.tsx.snap | 6 + .../src/main/js/components/docs/DocImg.tsx | 30 ++++ .../main/js/components/docs/DocInclude.tsx | 70 ++++++++++ .../src/main/js/components/docs/DocLink.tsx | 18 +-- .../js/components/docs/DocMarkdownBlock.tsx | 20 ++- .../main/js/components/docs/DocParagraph.tsx | 35 +++++ .../main/js/components/docs/DocTooltip.tsx | 2 +- .../DocMarkdownBlock-test.tsx.snap | 4 +- server/sonar-web/tsconfig.json | 6 +- server/sonar-web/yarn.lock | 55 +++++++- .../resources/org/sonar/l10n/core.properties | 8 ++ 62 files changed, 960 insertions(+), 55 deletions(-) create mode 100644 server/sonar-docs/src/EmbedDocsSuggestions.json create mode 100644 server/sonar-web/config/documentation-loader/fetch-matter.js create mode 100644 server/sonar-web/config/documentation-loader/index.js create mode 100644 server/sonar-web/config/documentation-loader/parse-directory.js create mode 100644 server/sonar-web/src/main/js/@types/gray-matter.d.ts delete mode 100644 server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/App.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js create mode 100644 server/sonar-web/src/main/js/apps/documentation/routes.ts create mode 100644 server/sonar-web/src/main/js/apps/documentation/utils.ts create mode 100644 server/sonar-web/src/main/js/components/docs/DocImg.tsx create mode 100644 server/sonar-web/src/main/js/components/docs/DocInclude.tsx create mode 100644 server/sonar-web/src/main/js/components/docs/DocParagraph.tsx 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,13 +79,25 @@ 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: [ // `allowExternal: true` to remove files outside of the current dir production && new CleanWebpackPlugin([paths.appBuild], { allowExternal: true, verbose: false }), + production && + new CopyWebpackPlugin([ + { + from: paths.docImages, + to: paths.appBuild + '/images/embed-doc/images' + } + ]), + production && new CopyWebpackPlugin([ { 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>( + input: I | { content: I }, + options?: O +): matter.GrayMatterFile; + +declare namespace matter { + type Input = string | Buffer; + interface GrayMatterOption> { + 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 { + 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>( + file: string | { content: string }, + data: object, + options?: GrayMatterOption + ): 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>( + fp: string, + options?: GrayMatterOption + ): matter.GrayMatterFile; + + /** + * 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>( + str: string, + options?: GrayMatterOption + ): 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>( + str: string, + options?: GrayMatterOption + ): { 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 ( - -
-
+ +
+

The page you were looking for does not exist.

You may have mistyped the address or the page may have moved. @@ -35,6 +42,6 @@ export default function NotFound() {

- +
); } 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 { 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 = () => { + 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 (
+
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 (
+
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 { return (
+ {error &&
{error}
} 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 { return ( <> + 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 (
+ 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" > + { return ( <> +
+ + { return ( <> +
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`] = ` + + { + 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 ; + } + + if (this.state.notFound) { + return ; + } + + return ( +
+ +
+ ); + } + + 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 ( +
+ + + + + {({ top }) => ( +
+
+
+
+ +

{translate('documentation.page')}

+ +
+ +
+
+
+ )} +
+ +
+
{this.renderContent()}
+
+
+ ); + } +} 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 { + getMenuEntriesHierarchy = (root?: string): Array => { + 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 ( + + +

+ {entry.children.length > 0 && ( + + )} + {entry.title} +

+ + {opened && entry.children.map(entry => this.renderEntry(entry, depth + 1))} +
+ ); + }; + + render() { + return ( +
+
+ {this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))} +
+
+ ); + } +} 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; +} + +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, + root?: string +): Array { + 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 { return ( <> +
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 { const selectedIndex = this.getSelectedIndex(); return (
+ {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 { return (
+
{ return (
+ 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 (
+ + 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 (
+ + { return (
+
{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( { it('fetches quality gates', () => { fetchQualityGates.mockClear(); getGateForProject.mockClear(); - mount(); + shallow(); 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(); + const wrapper = shallow(); 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 { return (
+
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(); + shallow(); expect(handleRequiredAuthorization).toBeCalled(); }); it('fetches profiles', () => { searchQualityProfiles.mockClear(); - mount(); + shallow(); 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(); + const wrapper = shallow(); 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 { render() { return (
+
+ 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 { render() { return (
+ + 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 { const { loading, sysInfoData } = this.state; return (
+ { const { loading, paging, users } = this.state; return (
+
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" > + + { return (
+ {({ 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 { return ( <> +
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`] = ` + + ) { + const { alt, src, ...other } = props; + + if (process.env.NODE_ENV === 'development') { + return {alt}; + } + + return {alt}; +} 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 { + 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 ; + } +} 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) { 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 */} - - {children} - - [TODO] - - ); - } + if (href && href.startsWith('/')) { + return ( + + {children} + + ); } 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 (
+ {displayH1 &&

{parsed.data.title}

} { remark() + // .use(remarkInclude) .use(reactRenderer, { remarkReactComponents: { // do not render outer
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 }
); 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) { + 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 ; + } + } + + return

; +} 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 { 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`] = ` -

this is @@ -17,7 +17,7 @@ exports[`should render simple markdown 1`] = ` bold text -

+
`; 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" diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 696cafb5d08..5297bb3bedd 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2416,6 +2416,14 @@ api_documentation.changelog=Changelog api_documentation.search=Search by name... +#------------------------------------------------------------------------------ +# +# DOCUMENTATION PAGE +# +#------------------------------------------------------------------------------ +documentation.page=Documentation + + #------------------------------------------------------------------------------ # # CODE -- 2.39.5