aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/.eslintrc2
-rw-r--r--server/sonar-web/.flowconfig1
-rw-r--r--server/sonar-web/config/documentation-loader/index.js26
-rw-r--r--server/sonar-web/package.json4
-rw-r--r--server/sonar-web/src/main/js/@types/lunr.d.ts48
-rw-r--r--server/sonar-web/src/main/js/@types/md.d.ts (renamed from server/sonar-web/config/documentation-loader/parse-directory.js)9
-rw-r--r--server/sonar-web/src/main/js/@types/strip-markdown.d.ts (renamed from server/sonar-web/src/main/js/components/docs/DocParagraph.tsx)17
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/type.css1
-rw-r--r--server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/App.tsx93
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx78
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx85
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx (renamed from server/sonar-web/config/documentation-loader/fetch-matter.js)45
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap70
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap125
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap58
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap84
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/pages.ts50
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/routes.ts12
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/utils.ts101
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx3
-rwxr-xr-xserver/sonar-web/src/main/js/apps/securityReports/components/App.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap101
-rw-r--r--server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx30
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocInclude.tsx75
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocTooltip.tsx71
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/helpers/markdown.d.ts3
-rw-r--r--server/sonar-web/src/main/js/helpers/markdown.js19
-rw-r--r--server/sonar-web/tsconfig.json1
-rw-r--r--server/sonar-web/yarn.lock8
67 files changed, 1291 insertions, 501 deletions
diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc
index 801383c08c7..ee259345ce1 100644
--- a/server/sonar-web/.eslintrc
+++ b/server/sonar-web/.eslintrc
@@ -2,6 +2,6 @@
"extends": "sonarqube",
"rules": {
- "import/extensions": ["error", "never", { "json": "always" }]
+ "import/extensions": ["error", "never", { "json": "always", "md": "always" }]
}
}
diff --git a/server/sonar-web/.flowconfig b/server/sonar-web/.flowconfig
index 10bfeea3cc6..c2e2109093a 100644
--- a/server/sonar-web/.flowconfig
+++ b/server/sonar-web/.flowconfig
@@ -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/index.js b/server/sonar-web/config/documentation-loader/index.js
index d25477bedd9..bf184f6341e 100644
--- a/server/sonar-web/config/documentation-loader/index.js
+++ b/server/sonar-web/config/documentation-loader/index.js
@@ -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/package.json b/server/sonar-web/package.json
index 6c5cdc30674..8335254c05c 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -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",
@@ -111,6 +112,7 @@
"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",
@@ -148,7 +150,7 @@
"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
index 00000000000..1fd03395605
--- /dev/null
+++ b/server/sonar-web/src/main/js/@types/lunr.d.ts
@@ -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/config/documentation-loader/parse-directory.js b/server/sonar-web/src/main/js/@types/md.d.ts
index 35b37040d72..e036188c5c2 100644
--- a/server/sonar-web/config/documentation-loader/parse-directory.js
+++ b/server/sonar-web/src/main/js/@types/md.d.ts
@@ -17,8 +17,7 @@
* 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)));
-};
+declare module '*.md' {
+ const value: string;
+ export default value;
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx b/server/sonar-web/src/main/js/@types/strip-markdown.d.ts
index 54b34660482..80d69fb86bf 100644
--- a/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx
+++ b/server/sonar-web/src/main/js/@types/strip-markdown.d.ts
@@ -17,19 +17,6 @@
* 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} />;
+declare module 'strip-markdown' {
+ export default function stripMarkdown(): any;
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
index d02389be63d..069472c96e5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
index 72ce3a7917c..86a8372f886 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
@@ -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"
diff --git a/server/sonar-web/src/main/js/app/styles/init/type.css b/server/sonar-web/src/main/js/app/styles/init/type.css
index 53721ff4a90..0b6b2c66e48 100644
--- a/server/sonar-web/src/main/js/app/styles/init/type.css
+++ b/server/sonar-web/src/main/js/app/styles/init/type.css
@@ -142,6 +142,7 @@ strong {
mark {
background: none;
+ color: var(--baseFontColor);
font-weight: bold;
}
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx b/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx
index 9416f11a790..a6dca76e8aa 100644
--- a/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx
+++ b/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
index 68b6f9ae1ca..3f231ca8904 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
@@ -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>}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
index 4521cc0d810..0b07f5b597d 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
index 110956c04fb..76f3480c30a 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
@@ -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>
);
};
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx
index 1f9c3f803ab..ed473536094 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx
index 24ae4b31954..56e4a54d0ac 100644
--- a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx
@@ -20,13 +20,12 @@
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>
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
index a350ef15bae..e4763bf53d8 100644
--- a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
+++ b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
@@ -20,39 +20,40 @@
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
index 00000000000..613c390d788
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx
@@ -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
index 00000000000..30cfdc9fd94
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
@@ -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
index 00000000000..24a9a6feb8b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
@@ -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
index 00000000000..2f71253a2d3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
@@ -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
index 00000000000..370c39b376f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx
@@ -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
index 00000000000..d4151e45220
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx
@@ -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/config/documentation-loader/fetch-matter.js b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
index 806332edb2a..6fc596b466f 100644
--- a/server/sonar-web/config/documentation-loader/fetch-matter.js
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
@@ -17,29 +17,26 @@
* 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');
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Sidebar from '../Sidebar';
-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;
-};
+function createPage(title: string, relativeName: string, text = '') {
+ return { relativeName, title, order: -1, text, content: text };
+}
-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);
-};
+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
index 00000000000..dbc7ad1e5a0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
@@ -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
index 00000000000..7a2b86c3590
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap
@@ -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
index 00000000000..69473caa244
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap
@@ -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
index 00000000000..c1527eb3f9c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
@@ -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
index 00000000000..cb4eb61c9c1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/pages.ts
@@ -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');
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.ts b/server/sonar-web/src/main/js/apps/documentation/routes.ts
index 704e4739073..617422a5ce5 100644
--- a/server/sonar-web/src/main/js/apps/documentation/routes.ts
+++ b/server/sonar-web/src/main/js/apps/documentation/routes.ts
@@ -19,14 +19,8 @@
*/
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;
diff --git a/server/sonar-web/src/main/js/apps/documentation/utils.ts b/server/sonar-web/src/main/js/apps/documentation/utils.ts
index 114c670ee6e..854f09bce68 100644
--- a/server/sonar-web/src/main/js/apps/documentation/utils.ts
+++ b/server/sonar-web/src/main/js/apps/documentation/utils.ts
@@ -17,12 +17,14 @@
* 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;
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
index 699d99a114c..1d59ca8d8e6 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
@@ -18,9 +18,14 @@
* 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 })}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
index acc639fed84..a047f84b3b4 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
@@ -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"
>
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx
index 68096c99764..86e505b33b3 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
index 778362a0ff1..094f4735c81 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
@@ -86,7 +86,7 @@ exports[`should render actions for admin 1`] = `
/>
<DocTooltip
className="spacer-left"
- doc="organizations/add-organization-member"
+ doc={Promise {}}
/>
</div>
</MembersPageHeader>
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
index 10e2496c277..ed6bfdd185e 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
@@ -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>
)}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
index e4771d74e76..ecd6133cecb 100644
--- a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js
index 5c575d3cd23..6b68124e23d 100644
--- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap
index f87fd62309a..a13b8e16bf0 100644
--- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap
index 2e09cfa0f8c..f95d36d808b 100644
--- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
index de42184f96d..282cffbcd99 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap
index ccf8e090d57..bcc91125263 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap
@@ -12,7 +12,7 @@ exports[`renders 1`] = `
</h1>
<DocTooltip
className="spacer-left"
- doc="quality-gates/quality-gate-projects"
+ doc={Promise {}}
/>
</div>
<div
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
index cf31adcb621..392c713f6f9 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
@@ -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')}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap
index 03ed0f32b01..b59e95a99e2 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap
@@ -12,7 +12,7 @@ exports[`renders 1`] = `
</h1>
<DocTooltip
className="spacer-left"
- doc="quality-profiles/quality-profile-projects"
+ doc={Promise {}}
/>
</div>
<div
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx
index 674d46470a2..d96e0c3b08a 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx
@@ -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>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
index 0eebf3c693d..88733a585e9 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
index 6c6a6b1938b..ae076709354 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
@@ -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')
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
index 2f7d6c54079..967c9af56ff 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx
index b3b4c651758..2edc32c1c73 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
index ae1e57854f9..b5be91be330 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
index 4be2c924b0d..69fbf16ae2e 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx b/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
index 0dbf8ffed55..2395da7da87 100755
--- a/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
index 58c69975ee9..b0059c48b81 100644
--- a/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx
index 0d155ef9eee..934965c7559 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx
@@ -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>
}
/>
diff --git a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap
index 7e30dedc4a9..8ece74c899f 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
index 5b05edbeb4d..14347621bd5 100644
--- a/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
+++ b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
index b0aef67a7db..87309d6a317 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
@@ -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
index d499b27c22c..00000000000
--- a/server/sonar-web/src/main/js/components/docs/DocInclude.tsx
+++ /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} />;
- }
-}
diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
index 0bc5350b297..c61a7212c52 100644
--- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
@@ -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/DocTooltip.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
index 3f7d4ec0195..a912b43c7b3 100644
--- a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
@@ -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
);
}
}
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx
index 570b68e37c0..8e933bddbcd 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx
@@ -20,20 +20,11 @@
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 });
-});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
index c47b4d7fe4b..c84fe63dedf 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
@@ -7,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>
`;
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap
index f19fea4d065..2c996886133 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap
@@ -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"
diff --git a/server/sonar-web/src/main/js/helpers/markdown.d.ts b/server/sonar-web/src/main/js/helpers/markdown.d.ts
index 83116b7b6eb..75ba14e0397 100644
--- a/server/sonar-web/src/main/js/helpers/markdown.d.ts
+++ b/server/sonar-web/src/main/js/helpers/markdown.d.ts
@@ -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;
diff --git a/server/sonar-web/src/main/js/helpers/markdown.js b/server/sonar-web/src/main/js/helpers/markdown.js
index b13db15e2fe..b8991fbaa17 100644
--- a/server/sonar-web/src/main/js/helpers/markdown.js
+++ b/server/sonar-web/src/main/js/helpers/markdown.js
@@ -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;
+}
diff --git a/server/sonar-web/tsconfig.json b/server/sonar-web/tsconfig.json
index 11eaced5e1e..de52341049c 100644
--- a/server/sonar-web/tsconfig.json
+++ b/server/sonar-web/tsconfig.json
@@ -15,6 +15,7 @@
"sourceMap": true,
"baseUrl": ".",
"paths": {
+ "Docs/*": ["../sonar-docs/src/*"],
"*": ["./src/main/js/@types/*"]
}
},
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 9abe63c34b2..1dc261f22da 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -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"