]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10612 Create documentation space in the web app
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Thu, 26 Apr 2018 06:42:19 +0000 (08:42 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 3 May 2018 18:20:50 +0000 (20:20 +0200)
62 files changed:
server/sonar-docs/src/EmbedDocsSuggestions.json [new file with mode: 0644]
server/sonar-web/config/documentation-loader/fetch-matter.js [new file with mode: 0644]
server/sonar-web/config/documentation-loader/index.js [new file with mode: 0644]
server/sonar-web/config/documentation-loader/parse-directory.js [new file with mode: 0644]
server/sonar-web/config/paths.js
server/sonar-web/config/webpack.config.js
server/sonar-web/package.json
server/sonar-web/scripts/start.js
server/sonar-web/src/main/js/@types/gray-matter.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/NotFound.js
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json [deleted file]
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
server/sonar-web/src/main/js/app/styles/init/misc.css
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/account/components/Account.js
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
server/sonar-web/src/main/js/apps/code/components/App.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/component-measures/components/App.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/documentation/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/App.js
server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
server/sonar-web/src/main/js/apps/settings/components/App.js
server/sonar-web/src/main/js/apps/system/components/App.tsx
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/components/docs/DocImg.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/docs/DocInclude.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/docs/DocLink.tsx
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
server/sonar-web/src/main/js/components/docs/DocParagraph.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
server/sonar-web/tsconfig.json
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-docs/src/EmbedDocsSuggestions.json b/server/sonar-docs/src/EmbedDocsSuggestions.json
new file mode 100644 (file)
index 0000000..7455d98
--- /dev/null
@@ -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 (file)
index 0000000..657a07f
--- /dev/null
@@ -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 (file)
index 0000000..d25477b
--- /dev/null
@@ -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 (file)
index 0000000..35b3704
--- /dev/null
@@ -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)));
+};
index 9733ef712ffbced8181f471f57c8e14a08a305d6..d1acbdd2274e87fe4503502c4a5ebd20a10f781f 100644 (file)
@@ -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')
 };
index cc2bfe7e970c9e0e9cdea28b71cca5299501aeda..c5439551c5c2b19e4f56d09d901970b8deb54119 100644 (file)
@@ -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([
         {
index 69537bc59a506742a1396c3495fc79b4feda79f2..9f300502e183303cc35a320e2576608bfd48311d 100644 (file)
@@ -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",
index cef0a40eaabf59ff1216205f3b4582986b329e51..88fbd9edcffe182e614647cbcfe50f6fbbee7955 100644 (file)
@@ -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 (file)
index 0000000..ee5cb17
--- /dev/null
@@ -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;
index 3f1d81ac0be6611073d8140b9e274ca94719e0bb..37e2a27fa645a239c03cb81b0306d8ad144a06f4 100644 (file)
@@ -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>
   );
 }
index 4a75361d7e597d8eb8935e0ed9855877dd701dab..d81f6bf88066c0a4b0bf7660784fc5fe5bbf3611 100644 (file)
@@ -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 (file)
index ae1ce94..0000000
+++ /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 " }
-  ]
-}
index 833c382e066231a5d49c7a8faf23d95f399c1738..c400a69fabadde1af1aa9930b86971510627e1fa 100644 (file)
@@ -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 {
index 80c0438930e9a608e79941227cab8c7b11530182..bcdeb28cd2f71f494a644b6fe0430640cfdf4e08 100644 (file)
@@ -189,6 +189,10 @@ td.big-spacer-top {
   overflow: hidden;
 }
 
+.max-width-100 {
+  max-width: 100%;
+}
+
 .width-100 {
   width: 100%;
 }
index 6d38859ecd7d1bae53b85c6782e184283ee0930e..35ba6778ddfda50b1e77f8685e84d313b9a02caf 100644 (file)
@@ -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} />
index 596c837c096a2c7fc2b0413594d2d275c39847d4..0266a3efaf69b25729c7c1a2c34efb53dfe49223 100644 (file)
@@ -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">
index 0c2770ca719e4fcaf78c0962f1332f3c29b46e32..b8d27cf0e7fc91cf6dc6a521175aff8bc0bae2c8 100644 (file)
@@ -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} />
 
index 3c9ebfc25f40847807488dad2cf7a3ec79821b5a..99f607b359f945c8b165e6fc580d31f4b89f3366 100644 (file)
@@ -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>}
index 19441b292183c31649981f2a5e7d3874f817d788..f9ad0c2eafdac2245c57898787e565d8a481d1d1 100644 (file)
@@ -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>
index b18f85ca24fedc6f06ed80a8ca1d05d18c0c4a64..03e0dd641ec1dcd5fa037ff0be0da8994ad6d509 100644 (file)
@@ -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">
index a5a8537c675db951f8ed5130810f1e28fe442259..70583de970fb61a54530bd6c852f6328ca03bdc5 100644 (file)
@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
   className="layout-page"
   id="component-measures"
 >
+  <Suggestions
+    suggestions="component_measures"
+  />
   <HelmetWrapper
     defer={true}
     encodeSpecialCharacters={true}
index 79e4a5e5d671a5ca3ae82bc742ab5fa9c73ad3a3..620b3f6a52817c3d9bbf1a15e3b09d9d52ecbe3a 100644 (file)
@@ -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
index cb97413281e4bf863a741d48b4facdf59b5c7aa5..c9a08f12b4dcf9a9a00f5c5f31d8ebbe8518ae12 100644 (file)
@@ -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}
index f802bc704f812db3309ea8e0d39d388a2c34b8de..a8f87cf99416eebed63a3bb196a91c65b7b55e1d 100644 (file)
@@ -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} />
index 55f852e26899bd687567b93b3c318f8951ac45ad..6a3320cd1917dea439e9beea07829e6b5258b11f 100644 (file)
@@ -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 (file)
index 0000000..90b0bfb
--- /dev/null
@@ -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 (file)
index 0000000..6eb6a4c
--- /dev/null
@@ -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 (file)
index 0000000..447060a
--- /dev/null
@@ -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 (file)
index 0000000..704e473
--- /dev/null
@@ -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 (file)
index 0000000..70ec0d1
--- /dev/null
@@ -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')
+    );
+  });
+}
index dac3048bf4995e7763e86bb83d181dd4dd6e1693..e44998e4bde87ca4eca32256ad161bf175a72dcf 100644 (file)
@@ -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} />
index 3ab8f9023e86a3c3ab0c1f80bfd8f65a8be2ea67..df3a3a1374c5a0471658cb41b62fd45c620412f9 100644 (file)
@@ -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)}
index 9446bfe471c7d87703d2815202426c1ef192810f..8b151058a493ae073b41ea34a8af4730bdeb60c0 100644 (file)
@@ -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
index ff2bff3aa8ce2954f35bb3b991c978d26c5edf6e..097e642860fec08af7b62e887a353c91a8a37f7e 100644 (file)
@@ -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>
index 33e8bea47d0673bdc4910481d5e9292958e374d0..df020fbe8a416098a11fb55e68d4dff8458dd67a 100644 (file)
@@ -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}
index ed4397179b80a5f5fe1a7c0bb7e7cadfc34b2ed6..d79c2bd51c87e94b8e123f41a47e8fe649855e03 100644 (file)
@@ -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 />
index cc920d302869d2ddd541a21d3c1c30765737cdd0..567853ebe551985380b6b9ad59ebf08be8d02f7a 100644 (file)
@@ -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
index 71e184790814ba5efbf44cac84e7a0ce1e55ce80..1500e14c91dd2631af61ed832564ae6c80301a3f 100644 (file)
@@ -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}
index 4b6c1615d89e69639e5b17d010030138fcaaf8a8..9d1d86e32dccb93bc769422dd243724f1e6b2fca 100644 (file)
@@ -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 ? (
index ff0be9851504590f26b3266ea2699174c7f9b681..158e002447c6896f75eb389ef2f8e21084824c43 100644 (file)
@@ -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;
 }
index a1f5ed8163a5e574b102814d0d4c458c993b13cc..381e56d4fd1f4e32e884b32f79e960c81bd101ef 100644 (file)
@@ -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 />
index 42756844899341139647913fd7046b47e16e8a70..685ceb6f749a280ef5a2be98e94a65dfc9e597a6 100644 (file)
@@ -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');
index 24b422552fe5428cdfc8a2d6b46fa6ef616deced..978feca7bbaba859e8b82710a21ce3b90fc78570 100644 (file)
@@ -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
index 81e1034c736591b4963715df7f9b46957b235763..dbd4a441f63d78f09e5cf9cd099d81230d73835f 100644 (file)
@@ -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">
index a831d023512e5caf587515a1f773f77420b622e5..2fe1b621c9725ad26df4c59c3f596bcd41d31d2e 100644 (file)
@@ -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}
index 00e3599079b4b8cbfdf79eed2468f4ad095c31ab..ad96321df754af37ca17dfea1dccb64dd9641931 100644 (file)
@@ -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} />
index 07ce3c1d3254bbcfa8749fe90f1ac22b3996ab32..2d4b850ae9343ed828e8d9cdb3e1deb37828baf3 100644 (file)
@@ -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
index 1cf4ee7e669979bc06178b887c9cb6de657f2812..e0ea320365564482a5cb96a43e697905af568b6f 100644 (file)
@@ -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} />
index c955a2eef06ec375dc8d547219bd894593019b87..68ea461d4883c2a8bdd7ce9fc48571038ad42f72 100644 (file)
@@ -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}
index cb2b5e5f4d2e50567af7077a2ea3e157848f3870..0b79a15c48360272e29ed3eb3757bb553a2022b3 100644 (file)
@@ -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 }) => (
index 9d33f1486e9fdc586253ae68ac17fc491c2ff5e4..e29280e7d14d943b3624f483bf6b3967414413cf 100644 (file)
@@ -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">
index 43c7785c6e03f430fd11f18254068a5ab0599dae..e8934607b3e52aa9c1d95b116df3b5ceb94937e4 100644 (file)
@@ -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 (file)
index 0000000..4df9753
--- /dev/null
@@ -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 (file)
index 0000000..b74eff2
--- /dev/null
@@ -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} />;
+  }
+}
index fd19f91cd94c7bef5f3f03d094329dca16c50c66..17b99af9c4ebb0952abbe55b00037bb3326ef7ba 100644 (file)
@@ -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 (
index 7a74a9f4b101dcabb979ae58001ca444fd99bfdd..88fcfb65cc8d0a7ca1215fde824e8b70f6c7ebb4 100644 (file)
@@ -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 (file)
index 0000000..54b3466
--- /dev/null
@@ -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} />;
+}
index 0dc27dd9954ef13d92e9243e2dc660b07b639c10..5c719443490d3a26035ad458a7f3570f3eb8373b 100644 (file)
@@ -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 });
index 28a2441e53097b71bd27ae3e46d1a1216e20d51b..7d44e66dc156e2684b8f3f69be9c2812ccb13d86 100644 (file)
@@ -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>
 `;
index 4e877399ac58dce749d921e1f2b4c3987046a005..11eaced5e1ef071463bad63f59d980c1e8113741 100644 (file)
     "lib": ["es2017", "dom"],
     "module": "esnext",
     "moduleResolution": "node",
-    "sourceMap": true
+    "sourceMap": true,
+    "baseUrl": ".",
+    "paths": {
+      "*": ["./src/main/js/@types/*"]
+    }
   },
   "include": ["./src/main/js/**/*"]
 }
index 7bbd78832473d4f8ef0f2127d48ed08c09c72e42..3707c3c7e8c05d101d77b3cd25d92dbe1c1da9f8 100644 (file)
     "@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"
   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"
index 696cafb5d083b84f52c072af5eef45733878b086..5297bb3beddcf0a331adcee7db12068664d0fdf6 100644 (file)
@@ -2416,6 +2416,14 @@ api_documentation.changelog=Changelog
 api_documentation.search=Search by name...
 
 
+#------------------------------------------------------------------------------
+#
+# DOCUMENTATION PAGE
+#
+#------------------------------------------------------------------------------
+documentation.page=Documentation
+
+
 #------------------------------------------------------------------------------
 #
 # CODE