]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11013 Add search capabilities to the embedded documentation (#513)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 13 Jul 2018 10:27:02 +0000 (12:27 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 25 Jul 2018 18:21:20 +0000 (20:21 +0200)
70 files changed:
server/sonar-web/.eslintrc
server/sonar-web/.flowconfig
server/sonar-web/config/documentation-loader/fetch-matter.js [deleted file]
server/sonar-web/config/documentation-loader/index.js
server/sonar-web/config/documentation-loader/parse-directory.js [deleted file]
server/sonar-web/package.json
server/sonar-web/src/main/js/@types/lunr.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/@types/md.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/@types/strip-markdown.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
server/sonar-web/src/main/js/app/styles/init/type.css
server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx
server/sonar-web/src/main/js/apps/documentation/components/App.tsx
server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/pages.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/routes.ts
server/sonar-web/src/main/js/apps/documentation/utils.ts
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap
server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap
server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap
server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
server/sonar-web/src/main/js/components/docs/DocInclude.tsx [deleted file]
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
server/sonar-web/src/main/js/components/docs/DocParagraph.tsx [deleted file]
server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap
server/sonar-web/src/main/js/helpers/markdown.d.ts
server/sonar-web/src/main/js/helpers/markdown.js
server/sonar-web/tsconfig.json
server/sonar-web/yarn.lock

index 801383c08c731b4696df4b798a66ccf454977ac9..ee259345ce1a02d6be1d3c09fddeb03f776df32e 100644 (file)
@@ -2,6 +2,6 @@
   "extends": "sonarqube",
 
   "rules": {
-    "import/extensions": ["error", "never", { "json": "always" }]
+    "import/extensions": ["error", "never", { "json": "always", "md": "always" }]
   }
 }
index 10bfeea3cc64470f7a34d4ff23b869d1a6bfc64b..c2e2109093a537cdc236801493b0688787acdaa3 100644 (file)
@@ -7,6 +7,7 @@
 <PROJECT_ROOT>/node_modules/webassemblyjs
 <PROJECT_ROOT>/node/.*
 <PROJECT_ROOT>/.vscode/.*
+<PROJECT_ROOT>/src/main/js/apps/overview/qualityGate/QualityGate.js
 
 [include]
 
diff --git a/server/sonar-web/config/documentation-loader/fetch-matter.js b/server/sonar-web/config/documentation-loader/fetch-matter.js
deleted file mode 100644 (file)
index 806332e..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 { getFrontMatter } = require('../../src/main/js/helpers/markdown');
-
-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 = getFrontMatter(content);
-      return {
-        name: path.basename(file).slice(0, -3),
-        relativeName: file.slice(0, -3),
-        title: headerData.title || file,
-        order: headerData.order || -1,
-        scope: headerData.scope && headerData.scope.toLowerCase()
-      };
-    })
-    .sort(compare);
-};
index d25477bedd914e91352ca721298528d6daec7279..bf184f6341e5cd716055e33c6ede4d3cb7e233f0 100644 (file)
@@ -17,9 +17,9 @@
  * 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 parseDirectory = require('./parse-directory');
-const fetchMatter = require('./fetch-matter');
+const glob = require('glob-promise');
 
 module.exports = function(source) {
   this.cacheable();
@@ -31,9 +31,27 @@ module.exports = function(source) {
   const root = path.resolve(path.dirname(this.resourcePath), config.root);
   this.addContextDependency(root);
 
-  parseDirectory(root)
-    .then(files => fetchMatter(root, files))
+  glob(root + '/**/*.md')
+    .then(files => files.map(file => file.substr(root.length + 1)))
+    .then(files =>
+      files.map(file => ({
+        path: file.slice(0, -3),
+        content: handleIncludes(fs.readFileSync(root + '/' + file, 'utf8'), root)
+      }))
+    )
     .then(result => `module.exports = ${JSON.stringify(result)};`)
     .then(success)
     .catch(failure);
 };
+
+/**
+ * @param {string} content
+ * @param {string} root
+ * @returns {string}
+ */
+function handleIncludes(content, root) {
+  return content.replace(/@include (.+)/, (match, p) => {
+    const filePath = path.join(root, '..', `${p}.md`);
+    return fs.readFileSync(filePath, 'utf8');
+  });
+}
diff --git a/server/sonar-web/config/documentation-loader/parse-directory.js b/server/sonar-web/config/documentation-loader/parse-directory.js
deleted file mode 100644 (file)
index 35b3704..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 6c5cdc306742288d8bbb786cfb403cbf07d44d84..8335254c05c6d661c3ccae100bab9d42c60d6a7a 100644 (file)
@@ -21,6 +21,7 @@
     "intl-relativeformat": "2.1.0",
     "keymaster": "1.6.2",
     "lodash": "4.17.10",
+    "lunr": "2.3.0",
     "prop-types": "15.6.1",
     "react": "16.2.0",
     "react-day-picker": "7.1.8",
     "react-test-renderer": "16.2.0",
     "remark": "9.0.0",
     "remark-react": "4.0.3",
+    "strip-markdown": "3.0.1",
     "style-loader": "0.21.0",
     "ts-jest": "22.4.6",
     "ts-loader": "4.3.0",
     "coveragePathIgnorePatterns": ["<rootDir>/node_modules", "<rootDir>/tests"],
     "moduleFileExtensions": ["ts", "tsx", "js", "json"],
     "moduleNameMapper": {
-      "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
+      "^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
         "<rootDir>/config/jest/FileStub.js",
       "^.+\\.css$": "<rootDir>/config/jest/CSSStub.js"
     },
diff --git a/server/sonar-web/src/main/js/@types/lunr.d.ts b/server/sonar-web/src/main/js/@types/lunr.d.ts
new file mode 100644 (file)
index 0000000..1fd0339
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+declare module 'lunr' {
+  export interface Lunr {
+    add(doc: any): void;
+
+    field(field: string, options?: { boost?: number }): void;
+
+    ref(field: string): void;
+
+    metadataWhitelist?: string[];
+  }
+
+  export interface LunrInit {
+    (this: Lunr): void;
+  }
+
+  export interface LunrMatch {
+    ref: string;
+    score: number;
+    matchData: { metadata: any };
+  }
+
+  export interface LunrIndex {
+    search(query: string): LunrMatch[];
+  }
+
+  function lunr(initializer: LunrInit): LunrIndex;
+
+  export default lunr;
+}
diff --git a/server/sonar-web/src/main/js/@types/md.d.ts b/server/sonar-web/src/main/js/@types/md.d.ts
new file mode 100644 (file)
index 0000000..e036188
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+declare module '*.md' {
+  const value: string;
+  export default value;
+}
diff --git a/server/sonar-web/src/main/js/@types/strip-markdown.d.ts b/server/sonar-web/src/main/js/@types/strip-markdown.d.ts
new file mode 100644 (file)
index 0000000..80d69fb
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+declare module 'strip-markdown' {
+  export default function stripMarkdown(): any;
+}
index d02389be63dbc4bba19503d740cc6f00fa4f7978..069472c96e5a0ba6db3df710856bd1772b550a7e 100644 (file)
@@ -181,7 +181,9 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
               fill={theme.gray80}
             />
             <span className="note">{displayName}</span>
-            <DocTooltip className="spacer-left" doc="branches/no-branch-support">
+            <DocTooltip
+              className="spacer-left"
+              doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/branches/no-branch-support.md')}>
               <PlusCircleIcon fill={theme.gray71} size={12} />
             </DocTooltip>
           </div>
@@ -193,7 +195,9 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
           <div className="navbar-context-branches">
             <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
             <span className="note">{displayName}</span>
-            <DocTooltip className="spacer-left" doc="branches/single-branch">
+            <DocTooltip
+              className="spacer-left"
+              doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/branches/single-branch.md')}>
               <PlusCircleIcon fill={theme.blue} size={12} />
             </DocTooltip>
           </div>
index 72ce3a7917c7c5c9ca43a8b6e549397daed89de4..86a8372f88648d1361b5348b2ef07c7d4aef77a1 100644 (file)
@@ -68,7 +68,7 @@ exports[`renders main branch 1`] = `
 exports[`renders no branch support popup 1`] = `
 <DocTooltip
   className="spacer-left"
-  doc="branches/no-branch-support"
+  doc={Promise {}}
 >
   <PlusCircleIcon
     fill="#b4b4b4"
@@ -270,7 +270,7 @@ exports[`renders short-living branch 1`] = `
 exports[`renders single branch popup 1`] = `
 <DocTooltip
   className="spacer-left"
-  doc="branches/single-branch"
+  doc={Promise {}}
 >
   <PlusCircleIcon
     fill="#4b9fd5"
index 53721ff4a90f1b3bf47adbfc92fb47547d4fb69e..0b6b2c66e4880ddcaf132da7adf43ea79effb615 100644 (file)
@@ -142,6 +142,7 @@ strong {
 
 mark {
   background: none;
+  color: var(--baseFontColor);
   font-weight: bold;
 }
 
index 9416f11a790b6d4583407230129d916383c4ea60..a6dca76e8aa8b8a12d62802f41f303a2d142c40f 100644 (file)
@@ -129,7 +129,10 @@ class CreateOrganizationForm extends React.PureComponent<Props, State> {
         <header className="modal-head">
           <h2>
             {translate('my_account.create_organization')}
-            <DocTooltip className="spacer-left" doc="organizations/organization" />
+            <DocTooltip
+              className="spacer-left"
+              doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/organization.md')}
+            />
           </h2>
         </header>
 
index 68b6f9ae1ca51eb8dbb76eae3752948b9fc286b9..3f231ca8904c42be2a8ea2fc346732eb0813238a 100644 (file)
@@ -163,7 +163,10 @@ export default class ProfileFacet extends React.PureComponent<Props> {
           onClick={this.handleHeaderClick}
           open={this.props.open}
           values={this.getTextValue()}>
-          <DocTooltip className="spacer-left" doc="rules/rules-quality-profiles" />
+          <DocTooltip
+            className="spacer-left"
+            doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/rules/rules-quality-profiles.md')}
+          />
         </FacetHeader>
 
         {this.props.open && <FacetItemsList>{profiles.map(this.renderItem)}</FacetItemsList>}
index 4521cc0d8103fafda701da35343584e057cfea9e..0b07f5b597d4122552c3bea7f13e8d97283e733c 100644 (file)
@@ -217,7 +217,10 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
                       onClick={onClick}>
                       {translate('delete')}
                     </Button>
-                    <DocTooltip className="spacer-left" doc="rules/custom-rule-removal" />
+                    <DocTooltip
+                      className="spacer-left"
+                      doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/rules/custom-rule-removal.md')}
+                    />
                   </>
                 )}
               </ConfirmButton>
index 110956c04fb00b4108da17cb39177f852cb4304c..76f3480c30af4c433c41c472a3776d5bab854177 100644 (file)
@@ -173,7 +173,10 @@ export default class RuleDetailsMeta extends React.PureComponent<Props> {
           {translate('coding_rules.show_template')}
         </Link>
         {')'}
-        <DocTooltip className="little-spacer-left" doc="rules/custom-rules" />
+        <DocTooltip
+          className="little-spacer-left"
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/rules/custom-rules.md')}
+        />
       </li>
     );
   };
index 1f9c3f803ab081fa0595e00edc7b8c9c3e30fe13..ed4735360941d1dadc64b005898405b12c972131 100644 (file)
@@ -57,7 +57,10 @@ export default class TemplateFacet extends React.PureComponent<Props> {
         renderTextName={this.renderName}
         singleSelection={true}
         values={value !== undefined ? [String(value)] : []}>
-        <DocTooltip className="spacer-left" doc="rules/rule-templates" />
+        <DocTooltip
+          className="spacer-left"
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/rules/rule-templates.md')}
+        />
       </Facet>
     );
   }
index 24ae4b319547a8a9f74d7d45c7c31c538d73bca1..56e4a54d0ac150891a46ef4763b66b7ea2d7a295 100644 (file)
 import * as React from 'react';
 import Helmet from 'react-helmet';
 import { Link } from 'react-router';
-import Menu from './Menu';
+import Sidebar from './Sidebar';
+import getPages from '../pages';
 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';
-import { getFrontMatter } from '../../../helpers/markdown';
 import { isSonarCloud } from '../../../helpers/system';
 import '../styles.css';
 
@@ -34,90 +33,48 @@ interface Props {
   params: { splat?: string };
 }
 
-interface State {
-  content?: string;
-  loading: boolean;
-  notFound: boolean;
-}
-
-export default class App extends React.PureComponent<Props, State> {
+export default class App extends React.PureComponent<Props> {
   mounted = false;
-
-  state: State = { loading: false, notFound: false };
+  pages = getPages();
 
   componentDidMount() {
-    this.mounted = true;
-    this.fetchContent(this.props.params.splat || 'index');
-
     const footer = document.getElementById('footer');
     if (footer) {
       footer.classList.add('page-footer-with-sidebar', 'documentation-footer');
     }
   }
 
-  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;
-
     const footer = document.getElementById('footer');
     if (footer) {
       footer.classList.remove('page-footer-with-sidebar', 'documentation-footer');
     }
   }
 
-  fetchContent = (path: string) => {
-    this.setState({ loading: true });
-    import(`Docs/pages/${path === '' ? 'index' : path}.md`).then(
-      ({ default: content }) => {
-        if (this.mounted) {
-          const { scope } = getFrontMatter(content || '');
-          if (scope === 'sonarcloud' && !isSonarCloud()) {
-            this.setState({ loading: false, notFound: true });
-          } else {
-            this.setState({ content, loading: false, notFound: false });
-          }
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false, notFound: true });
-        }
-      }
-    );
-  };
+  render() {
+    const { splat = 'index' } = this.props.params;
+    const page = this.pages.find(p => p.relativeName === splat);
+    const mainTitle = translate('documentation.page');
 
-  renderContent() {
-    if (this.state.notFound) {
-      return <NotFound withContainer={false} />;
+    if (!page) {
+      return (
+        <>
+          <Helmet title={mainTitle}>
+            <meta content="noindex nofollow" name="robots" />
+          </Helmet>
+          <NotFound withContainer={false} />
+        </>
+      );
     }
 
-    return (
-      <div className="boxed-group">
-        <DocMarkdownBlock
-          className="documentation-content cut-margins boxed-group-inner"
-          content={this.state.content}
-          displayH1={true}
-        />
-      </div>
-    );
-  }
+    const isIndex = splat === 'index';
 
-  render() {
-    const pageTitle = getFrontMatter(this.state.content || '').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}`}>
+        <Helmet title={isIndex ? mainTitle : `${page.title} - ${mainTitle}`}>
           {!isSonarCloud() && <meta content="noindex nofollow" name="robots" />}
         </Helmet>
+
         <ScreenPositionHelper className="layout-page-side-outer">
           {({ top }) => (
             <div className="layout-page-side" style={{ top }}>
@@ -128,7 +85,7 @@ export default class App extends React.PureComponent<Props, State> {
                       <h1>{translate('documentation.page')}</h1>
                     </Link>
                   </div>
-                  <Menu splat={this.props.params.splat} />
+                  <Sidebar pages={this.pages} splat={splat} />
                 </div>
               </div>
             </div>
@@ -137,7 +94,13 @@ export default class App extends React.PureComponent<Props, State> {
 
         <div className="layout-page-main">
           <div className="layout-page-main-inner documentation-layout-inner">
-            <DeferredSpinner loading={this.state.loading}>{this.renderContent()}</DeferredSpinner>
+            <div className="boxed-group">
+              <DocMarkdownBlock
+                className="documentation-content cut-margins boxed-group-inner"
+                content={page.content}
+                displayH1={true}
+              />
+            </div>
           </div>
         </div>
       </div>
index a350ef15baeeebf1d45c56d501cc8d6ac1b847f0..e4763bf53d87500dd93cd66201d223095f07aa6a 100644 (file)
 import * as React from 'react';
 import { Link } from 'react-router';
 import * as classNames from 'classnames';
-import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';
+import { sortBy } from 'lodash';
 import {
-  activeOrChildrenActive,
-  DocumentationEntry,
   getEntryChildren,
+  DocumentationEntry,
+  activeOrChildrenActive,
   getEntryRoot
 } from '../utils';
-import * as Docs from '../documentation.directory-loader';
-import { isSonarCloud } from '../../../helpers/system';
-
-const pages = (Docs as any) as DocumentationEntry[];
+import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';
 
 interface Props {
-  splat?: string;
+  pages: DocumentationEntry[];
+  splat: string;
 }
 
+type EntryWithChildren = DocumentationEntry & { children?: DocumentationEntry[] };
+
 export default class Menu extends React.PureComponent<Props> {
-  getMenuEntriesHierarchy = (root?: string): Array<DocumentationEntry> => {
-    const instancePages = isSonarCloud()
-      ? pages
-      : pages.filter(page => page.scope !== 'sonarcloud');
-    const toplevelEntries = getEntryChildren(instancePages, root);
-    toplevelEntries.forEach(entry => {
-      const entryRoot = getEntryRoot(entry.relativeName);
-      entry.children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
-    });
-    return toplevelEntries.sort((a, b) => parseInt(a.order, 10) - parseInt(b.order, 10));
+  getMenuEntriesHierarchy = (root?: string): EntryWithChildren[] => {
+    const topLevelEntries = getEntryChildren(this.props.pages, root);
+    return sortBy(
+      topLevelEntries.map(entry => {
+        const entryRoot = getEntryRoot(entry.relativeName);
+        const children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
+        return { ...entry, children };
+      }),
+      entry => entry.order
+    );
   };
 
-  renderEntry = (entry: DocumentationEntry, depth: number): React.ReactNode => {
+  renderEntry = (entry: EntryWithChildren, depth: number): React.ReactNode => {
     const active = entry.relativeName === this.props.splat;
     const opened = activeOrChildrenActive(this.props.splat || '', entry);
     const offset = 10 + 25 * depth;
+    const { children = [] } = entry;
     return (
       <React.Fragment key={entry.relativeName}>
         <Link
@@ -60,24 +61,16 @@ export default class Menu extends React.PureComponent<Props> {
           style={{ paddingLeft: offset }}
           to={'/documentation/' + entry.relativeName}>
           <h3 className="list-group-item-heading">
-            {entry.children.length > 0 && (
-              <OpenCloseIcon className="little-spacer-right" open={opened} />
-            )}
+            {children.length > 0 && <OpenCloseIcon className="little-spacer-right" open={opened} />}
             {entry.title}
           </h3>
         </Link>
-        {opened && entry.children.map(entry => this.renderEntry(entry, depth + 1))}
+        {opened && 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>
-    );
+    return <>{this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))}</>;
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx
new file mode 100644 (file)
index 0000000..613c390
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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 classNames from 'classnames';
+import { Link } from 'react-router';
+import { highlightMarks, cutWords, DocumentationEntry } from '../utils';
+
+export interface SearchResult {
+  page: DocumentationEntry;
+  highlights: { [field: string]: [number, number][] };
+}
+
+interface Props {
+  active: boolean;
+  result: SearchResult;
+}
+
+export default function SearchResultEntry({ active, result }: Props) {
+  return (
+    <Link
+      className={classNames('list-group-item', { active })}
+      to={'/documentation/' + result.page.relativeName}>
+      <SearchResultTitle result={result} />
+      <SearchResultText result={result} />
+    </Link>
+  );
+}
+
+export function SearchResultTitle({ result }: { result: SearchResult }) {
+  let titleWithMarks: React.ReactNode;
+
+  const titleHighlights = result.highlights.title;
+  if (titleHighlights && titleHighlights.length > 0) {
+    const { title } = result.page;
+    const tokens = highlightMarks(
+      title,
+      titleHighlights.map(h => ({ from: h[0], to: h[0] + h[1] }))
+    );
+    titleWithMarks = <SearchResultTokens tokens={tokens} />;
+  } else {
+    titleWithMarks = result.page.title;
+  }
+
+  return (
+    <h3 className="list-group-item-heading" style={{ fontWeight: 'normal' }}>
+      {titleWithMarks}
+    </h3>
+  );
+}
+
+export function SearchResultText({ result }: { result: SearchResult }) {
+  const textHighlights = result.highlights.text;
+  if (textHighlights && textHighlights.length > 0) {
+    const { text } = result.page;
+    const tokens = highlightMarks(text, textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] })));
+    return (
+      <div className="note">
+        <SearchResultTokens tokens={cutWords(tokens)} />
+      </div>
+    );
+  } else {
+    return null;
+  }
+}
+
+export function SearchResultTokens({
+  tokens
+}: {
+  tokens: Array<{ text: string; marked: boolean }>;
+}) {
+  return (
+    <>
+      {tokens.map((token, index) => (
+        <React.Fragment key={index}>
+          {token.marked ? <mark key={index}>{token.text}</mark> : token.text}
+        </React.Fragment>
+      ))}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
new file mode 100644 (file)
index 0000000..30cfdc9
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 lunr, { LunrIndex } from 'lunr';
+import SearchResultEntry, { SearchResult } from './SearchResultEntry';
+import { DocumentationEntry } from '../utils';
+
+interface Props {
+  pages: DocumentationEntry[];
+  query: string;
+  splat: string;
+}
+
+export default class SearchResults extends React.PureComponent<Props> {
+  index: LunrIndex;
+
+  constructor(props: Props) {
+    super(props);
+    this.index = lunr(function() {
+      this.ref('relativeName');
+      this.field('title', { boost: 10 });
+      this.field('text');
+
+      this.metadataWhitelist = ['position'];
+
+      props.pages.forEach(page => this.add(page));
+    });
+  }
+
+  render() {
+    const { query } = this.props;
+    const results = this.index
+      .search(`${query}~1 ${query}*`)
+      .map(match => {
+        const page = this.props.pages.find(page => page.relativeName === match.ref);
+        const highlights: { [field: string]: [number, number][] } = {};
+
+        Object.keys(match.matchData.metadata).forEach(term => {
+          Object.keys(match.matchData.metadata[term]).forEach(fieldName => {
+            const { position: positions } = match.matchData.metadata[term][fieldName];
+            highlights[fieldName] = [...(highlights[fieldName] || []), ...positions];
+          });
+        });
+
+        return { page, highlights };
+      })
+      .filter(result => result.page) as SearchResult[];
+
+    return (
+      <>
+        {results.map(result => (
+          <SearchResultEntry
+            active={result.page.relativeName === this.props.splat}
+            key={result.page.relativeName}
+            result={result}
+          />
+        ))}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
new file mode 100644 (file)
index 0000000..24a9a6f
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 Menu from './Menu';
+import SearchResults from './SearchResults';
+import { DocumentationEntry } from '../utils';
+import SearchBox from '../../../components/controls/SearchBox';
+
+interface Props {
+  pages: DocumentationEntry[];
+  splat: string;
+}
+
+interface State {
+  query: string;
+}
+
+export default class Sidebar extends React.PureComponent<Props, State> {
+  state: State = { query: '' };
+
+  handleSearch = (query: string) => {
+    this.setState({ query });
+  };
+
+  render() {
+    return (
+      <>
+        <SearchBox
+          className="big-spacer-top spacer-bottom"
+          minLength={2}
+          onChange={this.handleSearch}
+          placeholder="Search for pages or keywords"
+          value={this.state.query}
+        />
+        <div className="api-documentation-results panel">
+          <div className="list-group">
+            {this.state.query ? (
+              <SearchResults
+                pages={this.props.pages}
+                query={this.state.query}
+                splat={this.props.splat}
+              />
+            ) : (
+              <Menu pages={this.props.pages} splat={this.props.splat} />
+            )}
+          </div>
+        </div>
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
new file mode 100644 (file)
index 0000000..2f71253
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import Menu from '../Menu';
+
+function createPage(title: string, relativeName: string, text = '') {
+  return { relativeName, title, order: -1, text, content: text };
+}
+
+const pages = [
+  createPage(
+    'Lorem Ipsum',
+    'lorem/index',
+    "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
+  ),
+  createPage(
+    'Where does it come from?',
+    'lorem/origin',
+    'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.'
+  ),
+  createPage(
+    'Where does Foobar come from?',
+    'foobar',
+    'Foobar is a universal variable understood to represent whatever is being discussed.'
+  )
+];
+
+it('should render hierarchical menu', () => {
+  expect(shallow(<Menu pages={pages} splat="lorem/origin" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx
new file mode 100644 (file)
index 0000000..370c39b
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import SearchResultEntry, {
+  SearchResultText,
+  SearchResultTitle,
+  SearchResultTokens
+} from '../SearchResultEntry';
+
+const page = {
+  content: '',
+  order: -1,
+  relativeName: 'foo/bar',
+  text: 'Foobar is a universal variable understood to represent whatever is being discussed.',
+  title: 'Foobar'
+};
+
+describe('SearchResultEntry', () => {
+  it('should render', () => {
+    expect(
+      shallow(<SearchResultEntry active={true} result={{ page, highlights: {} }} />)
+    ).toMatchSnapshot();
+  });
+});
+
+describe('SearchResultText', () => {
+  it('should render with highlights', () => {
+    expect(
+      shallow(<SearchResultText result={{ page, highlights: { text: [[12, 9]] } }} />)
+    ).toMatchSnapshot();
+  });
+
+  it('should render without highlights', () => {
+    expect(shallow(<SearchResultText result={{ page, highlights: {} }} />)).toMatchSnapshot();
+  });
+});
+
+describe('SearchResultTitle', () => {
+  it('should render with highlights', () => {
+    expect(
+      shallow(<SearchResultTitle result={{ page, highlights: { title: [[0, 6]] } }} />)
+    ).toMatchSnapshot();
+  });
+
+  it('should render not without highlights', () => {
+    expect(shallow(<SearchResultTitle result={{ page, highlights: {} }} />)).toMatchSnapshot();
+  });
+});
+
+describe('SearchResultTokens', () => {
+  it('should render', () => {
+    expect(
+      shallow(
+        <SearchResultTokens
+          tokens={[
+            { marked: false, text: 'Foobar is a ' },
+            { marked: true, text: 'universal' },
+            {
+              marked: false,
+              text: ' variable understood to represent whatever is being discussed.'
+            }
+          ]}
+        />
+      )
+    ).toMatchSnapshot();
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx
new file mode 100644 (file)
index 0000000..d4151e4
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import lunr from 'lunr';
+import SearchResults from '../SearchResults';
+
+jest.mock('lunr', () => ({
+  default: jest.fn(() => ({
+    search: jest.fn(() => [
+      {
+        ref: 'lorem/origin',
+        matchData: {
+          metadata: { from: { title: { position: [[19, 5]] }, text: { position: [[121, 4]] } } }
+        }
+      },
+      { ref: 'foobar', matchData: { metadata: { from: { title: { position: [[23, 4]] } } } } }
+    ])
+  }))
+}));
+
+function createPage(title: string, relativeName: string, text = '') {
+  return { relativeName, title, order: -1, text, content: text };
+}
+
+const pages = [
+  createPage(
+    'Lorem Ipsum',
+    'lorem/index',
+    "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
+  ),
+  createPage(
+    'Where does it come from?',
+    'lorem/origin',
+    'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.'
+  ),
+  createPage(
+    'Where does Foobar come from?',
+    'foobar',
+    'Foobar is a universal variable understood to represent whatever is being discussed.'
+  )
+];
+
+it('should search', () => {
+  const wrapper = shallow(<SearchResults pages={pages} query="from" splat="foobar" />);
+  expect(wrapper).toMatchSnapshot();
+  expect(lunr).toBeCalled();
+  expect((wrapper.instance() as SearchResults).index.search).toBeCalledWith('from~1 from*');
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
new file mode 100644 (file)
index 0000000..6fc596b
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import Sidebar from '../Sidebar';
+
+function createPage(title: string, relativeName: string, text = '') {
+  return { relativeName, title, order: -1, text, content: text };
+}
+
+const pages = [
+  createPage('Lorem Ipsum', 'lorem/index'),
+  createPage('Where does Foobar come from?', 'foobar')
+];
+
+it('should render menu', () => {
+  expect(shallow(<Sidebar pages={pages} splat="foobar" />)).toMatchSnapshot();
+});
+
+it('should search', () => {
+  const wrapper = shallow(<Sidebar pages={pages} splat="foobar" />);
+  wrapper.find('SearchBox').prop<Function>('onChange')('foo');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
new file mode 100644 (file)
index 0000000..dbc7ad1
--- /dev/null
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render hierarchical menu 1`] = `
+<React.Fragment>
+  <React.Fragment
+    key="lorem/index"
+  >
+    <Link
+      className="list-group-item"
+      onlyActiveOnIndex={false}
+      style={
+        Object {
+          "paddingLeft": 10,
+        }
+      }
+      to="/documentation/lorem/index"
+    >
+      <h3
+        className="list-group-item-heading"
+      >
+        <OpenCloseIcon
+          className="little-spacer-right"
+          open={true}
+        />
+        Lorem Ipsum
+      </h3>
+    </Link>
+    <React.Fragment
+      key="lorem/origin"
+    >
+      <Link
+        className="list-group-item active"
+        onlyActiveOnIndex={false}
+        style={
+          Object {
+            "paddingLeft": 35,
+          }
+        }
+        to="/documentation/lorem/origin"
+      >
+        <h3
+          className="list-group-item-heading"
+        >
+          Where does it come from?
+        </h3>
+      </Link>
+    </React.Fragment>
+  </React.Fragment>
+  <React.Fragment
+    key="foobar"
+  >
+    <Link
+      className="list-group-item"
+      onlyActiveOnIndex={false}
+      style={
+        Object {
+          "paddingLeft": 10,
+        }
+      }
+      to="/documentation/foobar"
+    >
+      <h3
+        className="list-group-item-heading"
+      >
+        Where does Foobar come from?
+      </h3>
+    </Link>
+  </React.Fragment>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap
new file mode 100644 (file)
index 0000000..7a2b86c
--- /dev/null
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchResultEntry should render 1`] = `
+<Link
+  className="list-group-item active"
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to="/documentation/foo/bar"
+>
+  <SearchResultTitle
+    result={
+      Object {
+        "highlights": Object {},
+        "page": Object {
+          "content": "",
+          "order": -1,
+          "relativeName": "foo/bar",
+          "text": "Foobar is a universal variable understood to represent whatever is being discussed.",
+          "title": "Foobar",
+        },
+      }
+    }
+  />
+  <SearchResultText
+    result={
+      Object {
+        "highlights": Object {},
+        "page": Object {
+          "content": "",
+          "order": -1,
+          "relativeName": "foo/bar",
+          "text": "Foobar is a universal variable understood to represent whatever is being discussed.",
+          "title": "Foobar",
+        },
+      }
+    }
+  />
+</Link>
+`;
+
+exports[`SearchResultText should render with highlights 1`] = `
+<div
+  className="note"
+>
+  <SearchResultTokens
+    tokens={
+      Array [
+        Object {
+          "marked": false,
+          "text": "Foobar is a ",
+        },
+        Object {
+          "marked": true,
+          "text": "universal",
+        },
+        Object {
+          "marked": false,
+          "text": " variable understood to represent whatever is being discussed.",
+        },
+      ]
+    }
+  />
+</div>
+`;
+
+exports[`SearchResultText should render without highlights 1`] = `""`;
+
+exports[`SearchResultTitle should render not without highlights 1`] = `
+<h3
+  className="list-group-item-heading"
+  style={
+    Object {
+      "fontWeight": "normal",
+    }
+  }
+>
+  Foobar
+</h3>
+`;
+
+exports[`SearchResultTitle should render with highlights 1`] = `
+<h3
+  className="list-group-item-heading"
+  style={
+    Object {
+      "fontWeight": "normal",
+    }
+  }
+>
+  <SearchResultTokens
+    tokens={
+      Array [
+        Object {
+          "marked": true,
+          "text": "Foobar",
+        },
+      ]
+    }
+  />
+</h3>
+`;
+
+exports[`SearchResultTokens should render 1`] = `
+<React.Fragment>
+  <React.Fragment
+    key="0"
+  >
+    Foobar is a 
+  </React.Fragment>
+  <React.Fragment
+    key="1"
+  >
+    <mark
+      key="1"
+    >
+      universal
+    </mark>
+  </React.Fragment>
+  <React.Fragment
+    key="2"
+  >
+     variable understood to represent whatever is being discussed.
+  </React.Fragment>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap
new file mode 100644 (file)
index 0000000..69473ca
--- /dev/null
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should search 1`] = `
+<React.Fragment>
+  <SearchResultEntry
+    active={false}
+    key="lorem/origin"
+    result={
+      Object {
+        "highlights": Object {
+          "text": Array [
+            Array [
+              121,
+              4,
+            ],
+          ],
+          "title": Array [
+            Array [
+              19,
+              5,
+            ],
+          ],
+        },
+        "page": Object {
+          "content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.",
+          "order": -1,
+          "relativeName": "lorem/origin",
+          "text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.",
+          "title": "Where does it come from?",
+        },
+      }
+    }
+  />
+  <SearchResultEntry
+    active={true}
+    key="foobar"
+    result={
+      Object {
+        "highlights": Object {
+          "title": Array [
+            Array [
+              23,
+              4,
+            ],
+          ],
+        },
+        "page": Object {
+          "content": "Foobar is a universal variable understood to represent whatever is being discussed.",
+          "order": -1,
+          "relativeName": "foobar",
+          "text": "Foobar is a universal variable understood to represent whatever is being discussed.",
+          "title": "Where does Foobar come from?",
+        },
+      }
+    }
+  />
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
new file mode 100644 (file)
index 0000000..c1527eb
--- /dev/null
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render menu 1`] = `
+<React.Fragment>
+  <SearchBox
+    className="big-spacer-top spacer-bottom"
+    minLength={2}
+    onChange={[Function]}
+    placeholder="Search for pages or keywords"
+    value=""
+  />
+  <div
+    className="api-documentation-results panel"
+  >
+    <div
+      className="list-group"
+    >
+      <Menu
+        pages={
+          Array [
+            Object {
+              "content": "",
+              "order": -1,
+              "relativeName": "lorem/index",
+              "text": "",
+              "title": "Lorem Ipsum",
+            },
+            Object {
+              "content": "",
+              "order": -1,
+              "relativeName": "foobar",
+              "text": "",
+              "title": "Where does Foobar come from?",
+            },
+          ]
+        }
+        splat="foobar"
+      />
+    </div>
+  </div>
+</React.Fragment>
+`;
+
+exports[`should search 1`] = `
+<React.Fragment>
+  <SearchBox
+    className="big-spacer-top spacer-bottom"
+    minLength={2}
+    onChange={[Function]}
+    placeholder="Search for pages or keywords"
+    value="foo"
+  />
+  <div
+    className="api-documentation-results panel"
+  >
+    <div
+      className="list-group"
+    >
+      <SearchResults
+        pages={
+          Array [
+            Object {
+              "content": "",
+              "order": -1,
+              "relativeName": "lorem/index",
+              "text": "",
+              "title": "Lorem Ipsum",
+            },
+            Object {
+              "content": "",
+              "order": -1,
+              "relativeName": "foobar",
+              "text": "",
+              "title": "Where does Foobar come from?",
+            },
+          ]
+        }
+        query="foo"
+        splat="foobar"
+      />
+    </div>
+  </div>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/pages.ts b/server/sonar-web/src/main/js/apps/documentation/pages.ts
new file mode 100644 (file)
index 0000000..cb4eb61
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 remark from 'remark';
+import strip from 'strip-markdown';
+import { DocumentationEntry } from './utils';
+import * as Docs from './documentation.directory-loader';
+import { separateFrontMatter, filterContent } from '../../helpers/markdown';
+import { isSonarCloud } from '../../helpers/system';
+
+export default function getPages(): DocumentationEntry[] {
+  return Docs.map((file: any) => {
+    const parsed = separateFrontMatter(file.content);
+    const content = filterContent(parsed.content);
+
+    const text = remark()
+      .use(strip)
+      .processSync(content)
+      .contents.replace(/\n+/, ' ')
+      .trim();
+
+    return {
+      relativeName: file.path,
+      title: parsed.frontmatter.title,
+      order: Number(parsed.frontmatter.order || -1),
+      scope:
+        parsed.frontmatter.scope && parsed.frontmatter.scope.toLowerCase() === 'sonarcloud'
+          ? ('sonarcloud' as 'sonarcloud')
+          : undefined,
+      text,
+      content: file.content
+    };
+  }).filter((page: DocumentationEntry) => isSonarCloud() || page.scope !== 'sonarcloud');
+}
index 704e47390735e22ce2e5640719dab138a0ba93e4..617422a5ce5c0c8ddb68f2143c7cd1cfa59935d3 100644 (file)
  */
 import { lazyLoad } from '../../components/lazyLoad';
 
-const routes = [
-  {
-    indexRoute: { component: lazyLoad(() => import('./components/App')) }
-  },
-  {
-    path: '**',
-    indexRoute: { component: lazyLoad(() => import('./components/App')) }
-  }
-];
+const App = lazyLoad(() => import('./components/App'));
+
+const routes = [{ indexRoute: { component: App } }, { path: '**', indexRoute: { component: App } }];
 
 export default routes;
index 114c670ee6e7228876cd7006601b3eacaaaa6e58..854f09bce682540ed55a31fb543b4b9c6c6bd3d3 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { sortBy } from 'lodash';
+
 export interface DocumentationEntry {
-  children: DocumentationEntry[];
-  name: string;
-  order: string;
+  content: string;
+  order: number;
   relativeName: string;
   scope?: 'sonarcloud';
+  text: string;
   title: string;
 }
 
@@ -50,3 +52,96 @@ export function getEntryChildren(entries: DocumentationEntry[], root?: string) {
     );
   });
 }
+
+const WORDS = 6;
+
+function cutLeadingWords(str: string) {
+  let words = 0;
+  for (let i = str.length - 1; i >= 0; i--) {
+    if (/\s/.test(str[i])) {
+      words++;
+    }
+    if (words === WORDS) {
+      return i > 0 ? `...${str.substring(i + 1)}` : str;
+    }
+  }
+  return str;
+}
+
+function cutTrailingWords(str: string) {
+  let words = 0;
+  for (let i = 0; i < str.length; i++) {
+    if (/\s/.test(str[i])) {
+      words++;
+    }
+    if (words === WORDS) {
+      return i < str.length - 1 ? `${str.substring(0, i)}...` : str;
+    }
+  }
+  return str;
+}
+
+export function cutWords(tokens: Array<{ text: string; marked: boolean }>) {
+  const result: Array<{ text: string; marked: boolean }> = [];
+  let length = 0;
+
+  const highlightPos = tokens.findIndex(token => token.marked);
+  if (highlightPos > 0) {
+    const text = cutLeadingWords(tokens[highlightPos - 1].text);
+    result.push({ text, marked: false });
+    length += text.length;
+  }
+
+  result.push(tokens[highlightPos]);
+  length += tokens[highlightPos].text.length;
+
+  for (let i = highlightPos + 1; i < tokens.length; i++) {
+    if (length + tokens[i].text.length > 100) {
+      const text = cutTrailingWords(tokens[i].text);
+      result.push({ text, marked: false });
+      return result;
+    } else {
+      result.push(tokens[i]);
+      length += tokens[i].text.length;
+    }
+  }
+
+  return result;
+}
+
+export function highlightMarks(str: string, marks: Array<{ from: number; to: number }>) {
+  const sortedMarks = sortBy(
+    [
+      ...marks.map(mark => ({ pos: mark.from, start: true })),
+      ...marks.map(mark => ({ pos: mark.to, start: false }))
+    ],
+    mark => mark.pos,
+    mark => Number(!mark.start)
+  );
+
+  const cuts: Array<{ text: string; marked: boolean }> = [];
+  let start = 0;
+  let balance = 0;
+
+  for (const mark of sortedMarks) {
+    if (mark.start) {
+      if (balance === 0 && start !== mark.pos) {
+        cuts.push({ text: str.substring(start, mark.pos), marked: false });
+        start = mark.pos;
+      }
+      balance++;
+    } else {
+      balance--;
+      if (balance === 0 && start !== mark.pos) {
+        cuts.push({ text: str.substring(start, mark.pos), marked: true });
+        start = mark.pos;
+      }
+    }
+  }
+
+  if (start < str.length - 1) {
+    cuts.push({ text: str.substr(start), marked: false });
+  }
+
+  return cuts;
+}
index 699d99a114c8c1a41dcbaaeae8c75eff94fb4732..1d59ca8d8e6d19982ca948662f28c8e7b13880db 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import tooltipDCE from 'Docs/tooltips/editions/datacenter.md';
+import tooltipDE from 'Docs/tooltips/editions/developer.md';
+import tooltipEE from 'Docs/tooltips/editions/enterprise.md';
 import { Edition, getEditionUrl, EditionKey } from '../utils';
-import DocInclude from '../../../components/docs/DocInclude';
 import { translate } from '../../../helpers/l10n';
+import { lazyLoad } from '../../../components/lazyLoad';
+
+const DocMarkdownBlock = lazyLoad(() => import('../../../components/docs/DocMarkdownBlock'));
 
 interface Props {
   currentEdition?: EditionKey;
@@ -32,7 +37,9 @@ interface Props {
 export default function EditionBox({ edition, ncloc, serverId, currentEdition }: Props) {
   return (
     <div className="boxed-group boxed-group-inner marketplace-edition">
-      <DocInclude path={'/tooltips/editions/' + edition.key} />
+      {edition.key === 'datacenter' && <DocMarkdownBlock content={tooltipDCE} />}
+      {edition.key === 'developer' && <DocMarkdownBlock content={tooltipDE} />}
+      {edition.key === 'enterprise' && <DocMarkdownBlock content={tooltipEE} />}
       <div className="marketplace-edition-action spacer-top">
         <a
           href={getEditionUrl(edition, { ncloc, serverId, sourceEdition: currentEdition })}
index acc639fed84e70bb69e4d8b80d7f24db02e966f0..a047f84b3b4a47878475769542dec7b63596564e 100644 (file)
@@ -4,9 +4,7 @@ exports[`should display the edition 1`] = `
 <div
   className="boxed-group boxed-group-inner marketplace-edition"
 >
-  <DocInclude
-    path="/tooltips/editions/developer"
-  />
+  <LazyLoader />
   <div
     className="marketplace-edition-action spacer-top"
   >
index 68096c9976443025e7131e4fd5f5a448b30d0d49..86e505b33b3a4f32a398ce76cc3b833a8866e2d5 100644 (file)
@@ -90,7 +90,10 @@ export default class OrganizationMembers extends React.PureComponent<Props> {
                 memberLogins={this.props.memberLogins}
                 organization={organization}
               />
-              <DocTooltip className="spacer-left" doc="organizations/add-organization-member" />
+              <DocTooltip
+                className="spacer-left"
+                doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
+              />
             </div>
           )}
         </MembersPageHeader>
index 778362a0ff16e697171eec28a30cf540914bc85e..094f4735c81a9eebf8a843dd4b88a0e188aa165e 100644 (file)
@@ -86,7 +86,7 @@ exports[`should render actions for admin 1`] = `
       />
       <DocTooltip
         className="spacer-left"
-        doc="organizations/add-organization-member"
+        doc={Promise {}}
       />
     </div>
   </MembersPageHeader>
index 10e2496c277e11d1b8afc7e21001b7c4befa443c..ed6bfdd185e3a9257b9608a2a6b98e30c1a32ea9 100644 (file)
@@ -51,7 +51,9 @@ export default function OrganizationNavigationMeta({
       {onSonarCloud &&
         isPaidOrganization(organization) &&
         hasPrivateAccess(currentUser, organization, userOrganizations) && (
-          <DocTooltip className="spacer-right" doc="organizations/subscription-paid-plan">
+          <DocTooltip
+            className="spacer-right"
+            doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/subscription-paid-plan.md')}>
             <div className="outline-badge">{translate('organization.paid_plan.badge')}</div>
           </DocTooltip>
         )}
index e4771d74e76ac32a019461d3f6d68a8afe7f7845..ecd6133cecb8288dac7fb836f36442a7099b28d0 100644 (file)
@@ -91,7 +91,10 @@ export default class ApplicationQualityGate extends React.PureComponent<Props, S
         <h2 className="overview-title">
           {translate('overview.quality_gate')}
           {this.state.loading && <i className="spinner spacer-left" />}
-          <DocTooltip className="spacer-left" doc="quality-gates/project-homepage-quality-gate" />
+          <DocTooltip
+            className="spacer-left"
+            doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/project-homepage-quality-gate.md')}
+          />
           {status != null && <Level className="big-spacer-left" level={status} />}
         </h2>
 
index 5c575d3cd234a3bc8d932a7fd38f301f6878bc48..6b68124e23d4497e6f1c102523873f5c3ed70745 100644 (file)
@@ -65,7 +65,10 @@ export default function QualityGate({ branchLike, component, measures } /*: Prop
     <div className="overview-quality-gate" id="overview-quality-gate">
       <div className="display-flex-center">
         <h2 className="overview-title">{translate('overview.quality_gate')}</h2>
-        <DocTooltip className="spacer-left" doc="quality-gates/project-homepage-quality-gate" />
+        <DocTooltip
+          className="spacer-left"
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/project-homepage-quality-gate.md')}
+        />
         <Level className="big-spacer-left" level={level} />
       </div>
 
index f87fd62309a7ce78204753a97c3f3d5391eb9775..a13b8e16bf032350fc2b305e9c3bb342c65e7bfc 100644 (file)
@@ -14,7 +14,7 @@ exports[`renders 1`] = `
     />
     <DocTooltip
       className="spacer-left"
-      doc="quality-gates/project-homepage-quality-gate"
+      doc={Promise {}}
     />
   </h2>
 </div>
@@ -31,7 +31,7 @@ exports[`renders 2`] = `
     overview.quality_gate
     <DocTooltip
       className="spacer-left"
-      doc="quality-gates/project-homepage-quality-gate"
+      doc={Promise {}}
     />
     <Level
       className="big-spacer-left"
index 2e09cfa0f8c36d55a7163d3559aead5067ca57ff..f95d36d808be173ef62cd163035963ce3d1c4293 100644 (file)
@@ -15,7 +15,7 @@ exports[`renders message about ignored conditions 1`] = `
     </h2>
     <DocTooltip
       className="spacer-left"
-      doc="quality-gates/project-homepage-quality-gate"
+      doc={Promise {}}
     />
     <Level
       className="big-spacer-left"
index de42184f96dcb9dd1e31e686e0f301efaf64a001..282cffbcd992c00c569c8de632eed84792545066 100644 (file)
@@ -26,7 +26,10 @@ export default function Header() {
     <header className="page-header">
       <div className="page-title display-flex-center">
         <h1>{translate('project_quality_gate.page')}</h1>
-        <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-projects" />
+        <DocTooltip
+          className="spacer-left"
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/quality-gate-projects.md')}
+        />
       </div>
       <div className="page-description">{translate('project_quality_gate.page.description')}</div>
     </header>
index ccf8e090d5791ba8826d198bd4bd38019d68bc61..bcc911252633dc44596a1bb4f10c73fdf6f96f42 100644 (file)
@@ -12,7 +12,7 @@ exports[`renders 1`] = `
     </h1>
     <DocTooltip
       className="spacer-left"
-      doc="quality-gates/quality-gate-projects"
+      doc={Promise {}}
     />
   </div>
   <div
index cf31adcb6211d93b79b63cfaab2497ab23e68928..392c713f6f927381bd8dc06cc892cf271609450b 100644 (file)
@@ -26,7 +26,10 @@ export default function Header() {
     <header className="page-header">
       <div className="page-title display-flex-center">
         <h1>{translate('project_quality_profiles.page')}</h1>
-        <DocTooltip className="spacer-left" doc="quality-profiles/quality-profile-projects" />
+        <DocTooltip
+          className="spacer-left"
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/quality-profile-projects.md')}
+        />
       </div>
       <div className="page-description">
         {translate('project_quality_profiles.page.description')}
index 03ed0f32b011ccb216829a0cd151b15dbd59c51c..b59e95a99e2db8f835a9720c4905239129602d95 100644 (file)
@@ -12,7 +12,7 @@ exports[`renders 1`] = `
     </h1>
     <DocTooltip
       className="spacer-left"
-      doc="quality-profiles/quality-profile-projects"
+      doc={Promise {}}
     />
   </div>
   <div
index 674d46470a23edc6bc6881cf4654ff233f2dd6b6..d96e0c3b08a07fa25ff8b3ed7643faf2a241c129 100644 (file)
@@ -33,5 +33,10 @@ export default function BuiltInQualityGateBadge({ className }: Props) {
     </div>
   );
 
-  return <DocTooltip doc="quality-gates/built-in-quality-gate">{badge}</DocTooltip>;
+  return (
+    <DocTooltip
+      doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/built-in-quality-gate.md')}>
+      {badge}
+    </DocTooltip>
+  );
 }
index 0eebf3c693dce3db0470fda6df0f9a9edabd7bac..88733a585e9c79cbf17465a5c1285a9c822dcf9f 100644 (file)
@@ -100,7 +100,10 @@ export default class Conditions extends React.PureComponent<Props> {
         )}
         <header className="display-flex-center spacer-bottom">
           <h3>{translate('quality_gates.conditions')}</h3>
-          <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-conditions" />
+          <DocTooltip
+            className="spacer-left"
+            doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/quality-gate-conditions.md')}
+          />
         </header>
 
         <div className="big-spacer-bottom">{translate('quality_gates.introduction')}</div>
index 6c6a6b1938b9b429ca06232280d80679ca8940a6..ae076709354a7086fd020926fee4171164e8fdb7 100644 (file)
@@ -56,7 +56,10 @@ export default class DetailsContent extends React.PureComponent<Props> {
         <div className="quality-gate-section" id="quality-gate-projects">
           <header className="display-flex-center spacer-bottom">
             <h3>{translate('quality_gates.projects')}</h3>
-            <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-projects" />
+            <DocTooltip
+              className="spacer-left"
+              doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/quality-gate-projects.md')}
+            />
           </header>
           {isDefault ? (
             translate('quality_gates.projects_for_default')
index 2f7d6c54079421a0f784e59afecec776b0fc9b3c..967c9af56ff6562bb37e0f44f6e1d63670d73bee 100644 (file)
@@ -54,7 +54,10 @@ export default function ListHeader({ canCreate, refreshQualityGates, organizatio
 
       <div className="display-flex-center">
         <h1 className="page-title">{translate('quality_gates.page')}</h1>
-        <DocTooltip className="spacer-left" doc="quality-gates/quality-gate" />
+        <DocTooltip
+          className="spacer-left"
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/quality-gate.md')}
+        />
       </div>
     </header>
   );
index b3b4c6517580ca4c264b07edcc2f93ce4c9aed77..2edc32c1c73960e0d927e424ed37c53efa3f1710 100644 (file)
@@ -35,7 +35,12 @@ export default function BuiltInQualityProfileBadge({ className, tooltip = true }
   );
 
   if (tooltip) {
-    return <DocTooltip doc="quality-profiles/built-in-quality-profile">{badge}</DocTooltip>;
+    return (
+      <DocTooltip
+        doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/built-in-quality-profile.md')}>
+        {badge}
+      </DocTooltip>
+    );
   }
   return badge;
 }
index ae1e57854f94230d2650d89bd4b0cb5684c1d491..b5be91be3307f5d9e2587be764837e54bbcee51c 100644 (file)
@@ -66,7 +66,7 @@ export default class ProfilesList extends React.PureComponent<Props> {
             {translate('quality_profiles.list.projects')}
             <DocTooltip
               className="table-cell-doc"
-              doc="quality-profiles/quality-profile-projects"
+              doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/quality-profile-projects.md')}
             />
           </th>
           <th className="text-right nowrap">{translate('quality_profiles.list.rules')}</th>
index 4be2c924b0d7b0dcbb5c0bdb2aed853c1e525650..69fbf16ae2e4340404b5830564f80723c58890eb 100644 (file)
@@ -61,7 +61,8 @@ export default class ProfilesListRow extends React.PureComponent<Props> {
 
     if (profile.isDefault) {
       return (
-        <DocTooltip doc="quality-profiles/default-quality-profile">
+        <DocTooltip
+          doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/default-quality-profile.md')}>
           <span className="badge">{translate('default')}</span>
         </DocTooltip>
       );
index 0dbf8ffed55bc0367f1715b00955cd9eca5611eb..2395da7da870e9fafe9ac766277554c92f4009d7 100755 (executable)
@@ -146,7 +146,10 @@ export default class App extends React.PureComponent<Props, State> {
             onCheck={this.handleCheck}>
             <label className="little-spacer-left" htmlFor={'showCWE'}>
               {translate('security_reports.cwe.show')}
-              <DocTooltip className="spacer-left" doc="security-reports/cwe" />
+              <DocTooltip
+                className="spacer-left"
+                doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/security-reports/cwe.md')}
+              />
             </label>
           </Checkbox>
         </div>
index 58c69975ee910eff886a923a2bfd3b2801dc40df..b0059c48b8154c258466970c32e5b97b596ad700 100644 (file)
@@ -45,7 +45,7 @@ exports[`handle checkbox for cwe display 1`] = `
         security_reports.cwe.show
         <DocTooltip
           className="spacer-left"
-          doc="security-reports/cwe"
+          doc={Promise {}}
         />
       </label>
     </Checkbox>
@@ -115,7 +115,7 @@ exports[`handle checkbox for cwe display 2`] = `
         security_reports.cwe.show
         <DocTooltip
           className="spacer-left"
-          doc="security-reports/cwe"
+          doc={Promise {}}
         />
       </label>
     </Checkbox>
@@ -228,7 +228,7 @@ exports[`renders owaspTop10 1`] = `
         security_reports.cwe.show
         <DocTooltip
           className="spacer-left"
-          doc="security-reports/cwe"
+          doc={Promise {}}
         />
       </label>
     </Checkbox>
@@ -298,7 +298,7 @@ exports[`renders sansTop25 1`] = `
         security_reports.cwe.show
         <DocTooltip
           className="spacer-left"
-          doc="security-reports/cwe"
+          doc={Promise {}}
         />
       </label>
     </Checkbox>
@@ -368,7 +368,7 @@ exports[`renders with cwe 1`] = `
         security_reports.cwe.show
         <DocTooltip
           className="spacer-left"
-          doc="security-reports/cwe"
+          doc={Promise {}}
         />
       </label>
     </Checkbox>
index 0d155ef9eeea269b8579e20d7a8a3b3c8f01a4bb..934965c7559c521bd0fabb01a24821e87dcac310 100644 (file)
@@ -261,7 +261,10 @@ export default class OrganizationStep extends React.PureComponent<Props, State>
         stepTitle={
           <span>
             {translate('onboarding.organization.header')}
-            <DocTooltip className="little-spacer-left" doc="organizations/organization" />
+            <DocTooltip
+              className="little-spacer-left"
+              doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/organization.md')}
+            />
           </span>
         }
       />
index 7e30dedc4a95a2255ff3f2a590ae1db7e0ccd9a3..8ece74c899f206c15dfcb4f65529814b9175f8bd 100644 (file)
@@ -26,7 +26,7 @@ exports[`works with existing organization 1`] = `
         onboarding.organization.header
         <DocTooltip
           className="little-spacer-left"
-          doc="organizations/organization"
+          doc={Promise {}}
         />
       </span>
     }
@@ -47,103 +47,8 @@ exports[`works with existing organization 1`] = `
             onboarding.organization.header
             <DocTooltip
               className="little-spacer-left"
-              doc="organizations/organization"
-            >
-              <HelpTooltip
-                className="little-spacer-left"
-                onShow={[Function]}
-                overlay={
-                  <div
-                    className="abs-width-300"
-                  >
-                    <LazyLoader
-                      className="cut-margins"
-                      isTooltip={true}
-                    />
-                  </div>
-                }
-              >
-                <div
-                  className="help-tooltip little-spacer-left"
-                >
-                  <Tooltip
-                    mouseLeaveDelay={0.25}
-                    onShow={[Function]}
-                    overlay={
-                      <div
-                        className="abs-width-300"
-                      >
-                        <LazyLoader
-                          className="cut-margins"
-                          isTooltip={true}
-                        />
-                      </div>
-                    }
-                  >
-                    <TooltipInner
-                      mouseEnterDelay={0.1}
-                      mouseLeaveDelay={0.25}
-                      onShow={[Function]}
-                      overlay={
-                        <div
-                          className="abs-width-300"
-                        >
-                          <LazyLoader
-                            className="cut-margins"
-                            isTooltip={true}
-                          />
-                        </div>
-                      }
-                    >
-                      <span
-                        className="display-inline-flex-center"
-                        onMouseEnter={[Function]}
-                        onMouseLeave={[Function]}
-                      >
-                        <HelpIcon
-                          fill="#b4b4b4"
-                          size={12}
-                        >
-                          <Icon
-                            size={12}
-                          >
-                            <svg
-                              height={12}
-                              style={
-                                Object {
-                                  "clipRule": "evenodd",
-                                  "fillRule": "evenodd",
-                                  "strokeLinejoin": "round",
-                                  "strokeMiterlimit": "1.41421",
-                                }
-                              }
-                              version="1.1"
-                              viewBox="0 0 16 16"
-                              width={12}
-                              xmlSpace="preserve"
-                              xmlnsXlink="http://www.w3.org/1999/xlink"
-                            >
-                              <g
-                                transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)"
-                              >
-                                <path
-                                  d="M224,344L224,296C224,293.667 223.25,291.75 221.75,290.25C220.25,288.75 218.333,288 216,288L168,288C165.667,288 163.75,288.75 162.25,290.25C160.75,291.75 160,293.667 160,296L160,344C160,346.333 160.75,348.25 162.25,349.75C163.75,351.25 165.667,352 168,352L216,352C218.333,352 220.25,351.25 221.75,349.75C223.25,348.25 224,346.333 224,344ZM288,176C288,161.333 283.375,147.75 274.125,135.25C264.875,122.75 253.333,113.083 239.5,106.25C225.667,99.417 211.5,96 197,96C156.5,96 125.583,113.75 104.25,149.25C101.75,153.25 102.417,156.75 106.25,159.75L139.25,184.75C140.417,185.75 142,186.25 144,186.25C146.667,186.25 148.75,185.25 150.25,183.25C159.083,171.917 166.25,164.25 171.75,160.25C177.417,156.25 184.583,154.25 193.25,154.25C201.25,154.25 208.375,156.417 214.625,160.75C220.875,165.083 224,170 224,175.5C224,181.833 222.333,186.917 219,190.75C215.667,194.583 210,198.333 202,202C191.5,206.667 181.875,213.875 173.125,223.625C164.375,233.375 160,243.833 160,255L160,264C160,266.333 160.75,268.25 162.25,269.75C163.75,271.25 165.667,272 168,272L216,272C218.333,272 220.25,271.25 221.75,269.75C223.25,268.25 224,266.333 224,264C224,260.833 225.792,256.708 229.375,251.625C232.958,246.542 237.5,242.417 243,239.25C248.333,236.25 252.417,233.875 255.25,232.125C258.083,230.375 261.917,227.458 266.75,223.375C271.583,219.292 275.292,215.292 277.875,211.375C280.458,207.458 282.792,202.417 284.875,196.25C286.958,190.083 288,183.333 288,176ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z"
-                                  style={
-                                    Object {
-                                      "fill": "#b4b4b4",
-                                    }
-                                  }
-                                />
-                              </g>
-                            </svg>
-                          </Icon>
-                        </HelpIcon>
-                      </span>
-                    </TooltipInner>
-                  </Tooltip>
-                </div>
-              </HelpTooltip>
-            </DocTooltip>
+              doc={Promise {}}
+            />
           </span>
         </h2>
       </div>
index 5b05edbeb4d941fa18d51f39ac31ae8f1e66b4dc..14347621bd5362e9b2e3f782831bd861d5a07948 100644 (file)
@@ -82,20 +82,10 @@ export function PrivacyBadge({
   );
 
   if (onSonarCloud && organization) {
-    let docUrl = `project/visibility-${visibility}`;
-    if (visibility === Visibility.Public) {
-      if (icon) {
-        docUrl += '-paid-org';
-      }
-      if (organization.canAdmin) {
-        docUrl += '-admin';
-      }
-    }
-
     return (
       <DocTooltip
         className={className}
-        doc={docUrl}
+        doc={getDoc(visibility, icon, organization)}
         overlayProps={{ ...tooltipProps, organization: organization.key }}>
         {badge}
       </DocTooltip>
@@ -121,3 +111,21 @@ const mapStateToProps = (state: any, { organization }: OwnProps) => {
 };
 
 export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(PrivacyBadge);
+
+function getDoc(visibility: Visibility, icon: JSX.Element | null, organization: Organization) {
+  let doc;
+  if (visibility === Visibility.Private) {
+    doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-private.md');
+  } else if (icon) {
+    if (organization.canAdmin) {
+      doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public-paid-org-admin.md');
+    } else {
+      doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public-paid-org.md');
+    }
+  } else if (organization.canAdmin) {
+    doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public-admin.md');
+  } else {
+    doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public.md');
+  }
+  return doc;
+}
index b0aef67a7dbd6fe7a1b82075b402eee66fbee4fe..87309d6a317161be11edcda1361a4864ad825961 100644 (file)
@@ -16,7 +16,7 @@ exports[`renders 1`] = `
 
 exports[`renders public 1`] = `
 <DocTooltip
-  doc="project/visibility-public"
+  doc={Promise {}}
   overlayProps={
     Object {
       "organization": "foo",
@@ -33,7 +33,7 @@ exports[`renders public 1`] = `
 
 exports[`renders public with icon 1`] = `
 <DocTooltip
-  doc="project/visibility-public-paid-org-admin"
+  doc={Promise {}}
   overlayProps={
     Object {
       "organization": "foo",
diff --git a/server/sonar-web/src/main/js/components/docs/DocInclude.tsx b/server/sonar-web/src/main/js/components/docs/DocInclude.tsx
deleted file mode 100644 (file)
index d499b27..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 { lazyLoad } from '../lazyLoad';
-
-const DocMarkdownBlock = lazyLoad(() => import('./DocMarkdownBlock'));
-
-interface Props {
-  className?: string;
-  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 = () => {
-    // even if `this.props.path` starts with `/`,
-    // it is important to keep `Docs/` in the string to let webpack correctly resolve imports
-    import(`Docs/${this.props.path.substr(1)}.md`).then(
-      ({ default: content }) => {
-        if (this.mounted) {
-          this.setState({ content });
-        }
-      },
-      () => {}
-    );
-  };
-
-  render() {
-    return <DocMarkdownBlock className={this.props.className} content={this.state.content} />;
-  }
-}
index 0bc5350b29784ace40979147b199704d3518df53..c61a7212c528f4d949c61fb4ebe38ce3c9a6fdf1 100644 (file)
@@ -23,11 +23,9 @@ import remark from 'remark';
 import reactRenderer from 'remark-react';
 import remarkToc from 'remark-toc';
 import DocLink from './DocLink';
-import DocParagraph from './DocParagraph';
 import DocImg from './DocImg';
 import DocTooltipLink from './DocTooltipLink';
-import { separateFrontMatter } from '../../helpers/markdown';
-import { isSonarCloud } from '../../helpers/system';
+import { separateFrontMatter, filterContent } from '../../helpers/markdown';
 import { scrollToElement } from '../../helpers/scrolling';
 
 interface Props {
@@ -59,7 +57,6 @@ export default class DocMarkdownBlock extends React.PureComponent<Props> {
         {displayH1 && <h1>{parsed.frontmatter.title}</h1>}
         {
           remark()
-            // .use(remarkInclude)
             .use(remarkToc, { maxDepth: 3 })
             .use(reactRenderer, {
               remarkReactComponents: {
@@ -69,8 +66,6 @@ export default class DocMarkdownBlock extends React.PureComponent<Props> {
                 a: isTooltip
                   ? withChildProps(DocTooltipLink, childProps)
                   : withChildProps(DocLink, { onAnchorClick: this.handleAnchorClick }),
-                // used to handle `@include`
-                p: DocParagraph,
                 // use custom img tag to render documentation images
                 img: DocImg
               },
@@ -91,19 +86,3 @@ function withChildProps<P>(
     return <WrappedComponent customProps={childProps} {...props} />;
   };
 }
-
-function filterContent(content: string) {
-  const beginning = isSonarCloud() ? '<!-- sonarqube -->' : '<!-- sonarcloud -->';
-  const ending = isSonarCloud() ? '<!-- /sonarqube -->' : '<!-- /sonarcloud -->';
-
-  let newContent = content;
-  let start = newContent.indexOf(beginning);
-  let end = newContent.indexOf(ending);
-  while (start !== -1 && end !== -1) {
-    newContent = newContent.substring(0, start) + newContent.substring(end + ending.length);
-    start = newContent.indexOf(beginning);
-    end = newContent.indexOf(ending);
-  }
-
-  return newContent;
-}
diff --git a/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx b/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx
deleted file mode 100644 (file)
index 54b3466..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 3f7d4ec019547c03f73c33067490700e3d9dab4b..a912b43c7b3d1ec403b1e0dd8db36c521c2dc72a 100644 (file)
@@ -26,82 +26,55 @@ const DocMarkdownBlock = lazyLoad(() => import('./DocMarkdownBlock'));
 interface Props {
   className?: string;
   children?: React.ReactNode;
-  /** Key of the documentation chunk */
-  doc: string;
+  // Use as `import(/* webpackMode: "eager" */ 'Docs/tooltips/foo/bar.md')`
+  doc: Promise<{ default: string }>;
   overlayProps?: { [k: string]: string };
 }
 
 interface State {
   content?: string;
-  loading: boolean;
   open: boolean;
 }
 
 export default class DocTooltip extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false, open: false };
+  state: State = { open: false };
 
   componentDidMount() {
-    this.mounted = true;
+    this.props.doc.then(
+      ({ default: content }) => {
+        this.setState({ content });
+      },
+      () => {}
+    );
     document.addEventListener('scroll', this.close, true);
   }
 
-  componentWillReceiveProps(nextProps: Props) {
-    if (nextProps.doc !== this.props.doc) {
-      this.setState({ content: undefined, loading: false, open: false });
-    }
-  }
-
   componentWillUnmount() {
-    this.mounted = false;
     document.removeEventListener('scroll', this.close, true);
   }
 
-  fetchContent = () => {
-    this.setState({ loading: true });
-    import(`Docs/tooltips/${this.props.doc}.md`).then(
-      ({ default: content }) => {
-        if (this.mounted) {
-          this.setState({ content, loading: false });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
   close = () => {
     this.setState({ open: false });
   };
 
-  renderOverlay() {
-    return (
-      <div className="abs-width-300">
-        {this.state.loading ? (
-          <i className="spinner" />
-        ) : (
-          <DocMarkdownBlock
-            childProps={this.props.overlayProps}
-            className="cut-margins"
-            content={this.state.content}
-            isTooltip={true}
-          />
-        )}
-      </div>
-    );
-  }
-
   render() {
-    return (
+    return this.state.content ? (
       <HelpTooltip
         className={this.props.className}
-        onShow={this.fetchContent}
-        overlay={this.renderOverlay()}>
+        overlay={
+          <div className="abs-width-300">
+            <DocMarkdownBlock
+              childProps={this.props.overlayProps}
+              className="cut-margins"
+              content={this.state.content}
+              isTooltip={true}
+            />
+          </div>
+        }>
         {this.props.children}
       </HelpTooltip>
+    ) : (
+      this.props.children || null
     );
   }
 }
index 570b68e37c027e411aec57c03aeb2100a1d03a86..8e933bddbcdfb2331f823855f83c03e7a9bc8db6 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import DocTooltip from '../DocTooltip';
+import { waitAndUpdate } from '../../../helpers/testUtils';
 
-jest.useFakeTimers();
-
-it('should render', () => {
-  const wrapper = shallow(<DocTooltip doc="foo/bar" />);
-  wrapper.setState({ content: 'this is *bold* text', open: true, loading: true });
+it('should render', async () => {
+  const wrapper = shallow(<DocTooltip doc={Promise.resolve({ default: 'this is *bold* text' })} />);
   expect(wrapper).toMatchSnapshot();
-  wrapper.setState({ loading: false });
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
-
-it('should reset state when receiving new doc', () => {
-  const wrapper = shallow(<DocTooltip doc="foo/bar" />);
-  wrapper.setState({ content: 'this is *bold* text', open: true });
-  wrapper.setProps({ doc: 'baz' });
-  expect(wrapper.state()).toEqual({ content: undefined, loading: false, open: false });
-});
index c47b4d7fe4b5edd99236671c74675db51e659a8d..c84fe63dedf77eeb45a3760382002483bfbef57a 100644 (file)
@@ -7,39 +7,39 @@ exports[`should cut sonarqube/sonarcloud content 1`] = `
   <React.Fragment
     key="h-1"
   >
-    <DocParagraph
+    <p
       key="h-2"
     >
       some
-    </DocParagraph>
+    </p>
     
 
-    <DocParagraph
+    <p
       key="h-3"
     >
       sonarqube
-    </DocParagraph>
+    </p>
     
 
-    <DocParagraph
+    <p
       key="h-4"
     >
         long
-    </DocParagraph>
+    </p>
     
 
-    <DocParagraph
+    <p
       key="h-5"
     >
         multiline
-    </DocParagraph>
+    </p>
     
 
-    <DocParagraph
+    <p
       key="h-6"
     >
       text
-    </DocParagraph>
+    </p>
   </React.Fragment>
 </div>
 `;
@@ -51,25 +51,25 @@ exports[`should cut sonarqube/sonarcloud content 2`] = `
   <React.Fragment
     key="h-1"
   >
-    <DocParagraph
+    <p
       key="h-2"
     >
       some
-    </DocParagraph>
+    </p>
     
 
-    <DocParagraph
+    <p
       key="h-3"
     >
       sonarcloud
-    </DocParagraph>
+    </p>
     
 
-    <DocParagraph
+    <p
       key="h-4"
     >
       text
-    </DocParagraph>
+    </p>
   </React.Fragment>
 </div>
 `;
@@ -81,7 +81,7 @@ exports[`should render simple markdown 1`] = `
   <React.Fragment
     key="h-1"
   >
-    <DocParagraph
+    <p
       key="h-2"
     >
       this is 
@@ -91,7 +91,7 @@ exports[`should render simple markdown 1`] = `
         bold
       </em>
        text
-    </DocParagraph>
+    </p>
   </React.Fragment>
 </div>
 `;
index f19fea4d06569a06bbbb1fc52817a12cc55d5162..2c9968861339f0cbdd68783f328b2cde7a4ebd89 100644 (file)
@@ -1,23 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render 1`] = `
-<HelpTooltip
-  onShow={[Function]}
-  overlay={
-    <div
-      className="abs-width-300"
-    >
-      <i
-        className="spinner"
-      />
-    </div>
-  }
-/>
-`;
+exports[`should render 1`] = `""`;
 
 exports[`should render 2`] = `
 <HelpTooltip
-  onShow={[Function]}
   overlay={
     <div
       className="abs-width-300"
index 83116b7b6ebc068ff97e0569214a088fef24f17d..75ba14e0397a6151db4af817aefa157f9b0cd493 100644 (file)
@@ -24,3 +24,6 @@ interface FrontMatter {
 export function getFrontMatter(content: string): FrontMatter;
 
 export function separateFrontMatter(content: string): { content: string; frontmatter: FrontMatter };
+
+/** Removes SonarQube/SonarCloud only content */
+export function filterContent(content: string): string;
index b13db15e2fe934d1a175422698a0ecf189127344..b8991fbaa17aabfd6be913dc6b5cc0b5ca3d6985 100644 (file)
@@ -19,7 +19,7 @@
  */
 
 // keep this file in JavaScript, because it is used by a webpack loader
-module.exports = { getFrontMatter, separateFrontMatter };
+module.exports = { getFrontMatter, separateFrontMatter, filterContent };
 
 function getFrontMatter(content) {
   const lines = content.split('\n');
@@ -66,3 +66,20 @@ function parseFrontMatter(lines) {
   }
   return data;
 }
+
+function filterContent(content) {
+  const { isSonarCloud } = require('../helpers/system');
+  const beginning = isSonarCloud() ? '<!-- sonarqube -->' : '<!-- sonarcloud -->';
+  const ending = isSonarCloud() ? '<!-- /sonarqube -->' : '<!-- /sonarcloud -->';
+
+  let newContent = content;
+  let start = newContent.indexOf(beginning);
+  let end = newContent.indexOf(ending);
+  while (start !== -1 && end !== -1) {
+    newContent = newContent.substring(0, start) + newContent.substring(end + ending.length);
+    start = newContent.indexOf(beginning);
+    end = newContent.indexOf(ending);
+  }
+
+  return newContent;
+}
index 11eaced5e1ef071463bad63f59d980c1e8113741..de52341049c72cd31c02e58b4cf7d134d0f999da 100644 (file)
@@ -15,6 +15,7 @@
     "sourceMap": true,
     "baseUrl": ".",
     "paths": {
+      "Docs/*": ["../sonar-docs/src/*"],
       "*": ["./src/main/js/@types/*"]
     }
   },
index 9abe63c34b2d5abce5018e7d3793c46aa8bdb1f4..1dc261f22da93c6a6853cb9842a95d319b6ab89e 100644 (file)
@@ -5513,6 +5513,10 @@ lru-cache@^4.0.1, lru-cache@^4.1.1:
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
+lunr@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.0.tgz#4d7c0ca12bdd1e0447b0c131b91420929740c88f"
+
 macaddress@^0.2.8:
   version "0.2.8"
   resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
@@ -8201,6 +8205,10 @@ strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
+strip-markdown@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-markdown/-/strip-markdown-3.0.1.tgz#bf4f1c04d03720ae76a01a111cef941d5255cf88"
+
 style-loader@0.21.0:
   version "0.21.0"
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852"