From b998b44aafeff3726122bbc491e2c84aed284b61 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 13 Jul 2018 12:27:02 +0200 Subject: [PATCH] SONAR-11013 Add search capabilities to the embedded documentation (#513) --- server/sonar-web/.eslintrc | 2 +- server/sonar-web/.flowconfig | 1 + .../config/documentation-loader/index.js | 26 +++- server/sonar-web/package.json | 4 +- server/sonar-web/src/main/js/@types/lunr.d.ts | 48 +++++++ .../main/js/@types/md.d.ts} | 9 +- .../strip-markdown.d.ts} | 17 +-- .../nav/component/ComponentNavBranch.tsx | 8 +- .../ComponentNavBranch-test.tsx.snap | 4 +- .../src/main/js/app/styles/init/type.css | 1 + .../organizations/CreateOrganizationForm.tsx | 5 +- .../coding-rules/components/ProfileFacet.tsx | 5 +- .../coding-rules/components/RuleDetails.tsx | 5 +- .../components/RuleDetailsMeta.tsx | 5 +- .../coding-rules/components/TemplateFacet.tsx | 5 +- .../js/apps/documentation/components/App.tsx | 93 ++++--------- .../js/apps/documentation/components/Menu.tsx | 53 ++++---- .../components/SearchResultEntry.tsx | 97 ++++++++++++++ .../components/SearchResults.tsx | 78 +++++++++++ .../apps/documentation/components/Sidebar.tsx | 68 ++++++++++ .../components/__tests__/Menu-test.tsx | 48 +++++++ .../__tests__/SearchResultEntry-test.tsx | 85 ++++++++++++ .../__tests__/SearchResults-test.tsx | 66 +++++++++ .../components/__tests__/Sidebar-test.tsx} | 45 +++---- .../__snapshots__/Menu-test.tsx.snap | 70 ++++++++++ .../SearchResultEntry-test.tsx.snap | 125 ++++++++++++++++++ .../__snapshots__/SearchResults-test.tsx.snap | 58 ++++++++ .../__snapshots__/Sidebar-test.tsx.snap | 84 ++++++++++++ .../src/main/js/apps/documentation/pages.ts | 50 +++++++ .../src/main/js/apps/documentation/routes.ts | 12 +- .../src/main/js/apps/documentation/utils.ts | 101 +++++++++++++- .../marketplace/components/EditionBox.tsx | 11 +- .../__snapshots__/EditionBox-test.tsx.snap | 4 +- .../components/OrganizationMembers.tsx | 5 +- .../OrganizationMembers-test.tsx.snap | 2 +- .../navigation/OrganizationNavigationMeta.tsx | 4 +- .../qualityGate/ApplicationQualityGate.tsx | 5 +- .../apps/overview/qualityGate/QualityGate.js | 5 +- .../ApplicationQualityGate-test.tsx.snap | 4 +- .../__snapshots__/QualityGate-test.js.snap | 2 +- .../js/apps/projectQualityGate/Header.tsx | 5 +- .../__snapshots__/Header-test.tsx.snap | 2 +- .../js/apps/projectQualityProfiles/Header.tsx | 5 +- .../__snapshots__/Header-test.tsx.snap | 2 +- .../components/BuiltInQualityGateBadge.tsx | 7 +- .../quality-gates/components/Conditions.tsx | 5 +- .../components/DetailsContent.tsx | 5 +- .../quality-gates/components/ListHeader.tsx | 5 +- .../components/BuiltInQualityProfileBadge.tsx | 7 +- .../quality-profiles/home/ProfilesList.tsx | 2 +- .../quality-profiles/home/ProfilesListRow.tsx | 3 +- .../apps/securityReports/components/App.tsx | 5 +- .../__tests__/__snapshots__/App-test.tsx.snap | 10 +- .../projectOnboarding/OrganizationStep.tsx | 5 +- .../OrganizationStep-test.tsx.snap | 101 +------------- .../common/PrivacyBadgeContainer.tsx | 30 +++-- .../PrivacyBadgeContainer-test.tsx.snap | 4 +- .../main/js/components/docs/DocInclude.tsx | 75 ----------- .../js/components/docs/DocMarkdownBlock.tsx | 23 +--- .../main/js/components/docs/DocTooltip.tsx | 71 +++------- .../docs/__tests__/DocTooltip-test.tsx | 17 +-- .../DocMarkdownBlock-test.tsx.snap | 36 ++--- .../__snapshots__/DocTooltip-test.tsx.snap | 16 +-- .../src/main/js/helpers/markdown.d.ts | 3 + .../sonar-web/src/main/js/helpers/markdown.js | 19 ++- server/sonar-web/tsconfig.json | 1 + server/sonar-web/yarn.lock | 8 ++ 67 files changed, 1291 insertions(+), 501 deletions(-) create mode 100644 server/sonar-web/src/main/js/@types/lunr.d.ts rename server/sonar-web/{config/documentation-loader/parse-directory.js => src/main/js/@types/md.d.ts} (83%) rename server/sonar-web/src/main/js/{components/docs/DocParagraph.tsx => @types/strip-markdown.d.ts} (62%) create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx rename server/sonar-web/{config/documentation-loader/fetch-matter.js => src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx} (51%) create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/documentation/pages.ts delete mode 100644 server/sonar-web/src/main/js/components/docs/DocInclude.tsx 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 @@ /node_modules/webassemblyjs /node/.* /.vscode/.* +/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": ["/node_modules", "/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)$": "/config/jest/FileStub.js", "^.+\\.css$": "/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 similarity index 83% rename from server/sonar-web/config/documentation-loader/parse-directory.js rename to 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 similarity index 62% rename from server/sonar-web/src/main/js/components/docs/DocParagraph.tsx rename to 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) { - if (Array.isArray(props.children) && props.children.length === 1) { - const child = props.children[0]; - if (typeof child === 'string' && child.startsWith(INCLUDE)) { - const includePath = child.substr(INCLUDE.length + 1); - return ; - } - } - - return

; +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 {displayName} - + @@ -193,7 +195,9 @@ export default class ComponentNavBranch extends React.PureComponent {displayName} - + 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`] = ` {

{translate('my_account.create_organization')} - +

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 { onClick={this.handleHeaderClick} open={this.props.open} values={this.getTextValue()}> - + {this.props.open && {profiles.map(this.renderItem)}} 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 { onClick={onClick}> {translate('delete')} - + )} 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 { {translate('coding_rules.show_template')} {')'} - + ); }; 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 { renderTextName={this.renderName} singleSelection={true} values={value !== undefined ? [String(value)] : []}> - + ); } 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 { +export default class App extends React.PureComponent { 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 ; + if (!page) { + return ( + <> + + + + + + ); } - return ( -
- -
- ); - } + 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 (
- + {!isSonarCloud() && } + {({ top }) => (
@@ -128,7 +85,7 @@ export default class App extends React.PureComponent {

{translate('documentation.page')}

- +
@@ -137,7 +94,13 @@ export default class App extends React.PureComponent {
- {this.renderContent()} +
+ +
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx 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 { - getMenuEntriesHierarchy = (root?: string): Array => { - 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 ( { style={{ paddingLeft: offset }} to={'/documentation/' + entry.relativeName}>

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

- {opened && entry.children.map(entry => this.renderEntry(entry, depth + 1))} + {opened && children.map(entry => this.renderEntry(entry, depth + 1))}
); }; render() { - return ( -
-
- {this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))} -
-
- ); + 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 ( + + + + + ); +} + +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 = ; + } else { + titleWithMarks = result.page.title; + } + + return ( +

+ {titleWithMarks} +

+ ); +} + +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 ( +
+ +
+ ); + } else { + return null; + } +} + +export function SearchResultTokens({ + tokens +}: { + tokens: Array<{ text: string; marked: boolean }>; +}) { + return ( + <> + {tokens.map((token, index) => ( + + {token.marked ? {token.text} : token.text} + + ))} + + ); +} 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 { + 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 => ( + + ))} + + ); + } +} 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 { + state: State = { query: '' }; + + handleSearch = (query: string) => { + this.setState({ query }); + }; + + render() { + return ( + <> + +
+
+ {this.state.query ? ( + + ) : ( + + )} +
+
+ + ); + } +} 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()).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() + ).toMatchSnapshot(); + }); +}); + +describe('SearchResultText', () => { + it('should render with highlights', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); + + it('should render without highlights', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); + +describe('SearchResultTitle', () => { + it('should render with highlights', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); + + it('should render not without highlights', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); + +describe('SearchResultTokens', () => { + it('should render', () => { + expect( + shallow( + + ) + ).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(); + 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 similarity index 51% rename from server/sonar-web/config/documentation-loader/fetch-matter.js rename to 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()).toMatchSnapshot(); +}); + +it('should search', () => { + const wrapper = shallow(); + wrapper.find('SearchBox').prop('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`] = ` + + + +

+ + Lorem Ipsum +

+ + + +

+ Where does it come from? +

+ +
+
+ + +

+ Where does Foobar come from? +

+ +
+
+`; 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`] = ` + + + + +`; + +exports[`SearchResultText should render with highlights 1`] = ` +
+ +
+`; + +exports[`SearchResultText should render without highlights 1`] = `""`; + +exports[`SearchResultTitle should render not without highlights 1`] = ` +

+ Foobar +

+`; + +exports[`SearchResultTitle should render with highlights 1`] = ` +

+ +

+`; + +exports[`SearchResultTokens should render 1`] = ` + + + Foobar is a + + + + universal + + + + variable understood to represent whatever is being discussed. + + +`; 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`] = ` + + + + +`; 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`] = ` + + +
+
+ +
+
+
+`; + +exports[`should search 1`] = ` + + +
+
+ +
+
+
+`; 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 (
- + {edition.key === 'datacenter' && } + {edition.key === 'developer' && } + {edition.key === 'enterprise' && }
- +
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 { memberLogins={this.props.memberLogins} organization={organization} /> - +
)} 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`] = ` />
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) && ( - +
{translate('organization.paid_plan.badge')}
)} 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 {translate('overview.quality_gate')} {this.state.loading && } - + {status != null && } 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

{translate('overview.quality_gate')}

- +
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`] = ` />
@@ -31,7 +31,7 @@ exports[`renders 2`] = ` overview.quality_gate

{translate('project_quality_gate.page')}

- +
{translate('project_quality_gate.page.description')}
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`] = `

{translate('project_quality_profiles.page')}

- +
{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`] = `
); - return {badge}; + return ( + + {badge} + + ); } 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 { )}

{translate('quality_gates.conditions')}

- +
{translate('quality_gates.introduction')}
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 {

{translate('quality_gates.projects')}

- +
{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

{translate('quality_gates.page')}

- +
); 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 {badge}; + return ( + + {badge} + + ); } 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 { {translate('quality_profiles.list.projects')} {translate('quality_profiles.list.rules')} 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 { if (profile.isDefault) { return ( - + {translate('default')} ); 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 { onCheck={this.handleCheck}>
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 @@ -115,7 +115,7 @@ exports[`handle checkbox for cwe display 2`] = ` security_reports.cwe.show @@ -228,7 +228,7 @@ exports[`renders owaspTop10 1`] = ` security_reports.cwe.show @@ -298,7 +298,7 @@ exports[`renders sansTop25 1`] = ` security_reports.cwe.show @@ -368,7 +368,7 @@ exports[`renders with cwe 1`] = ` security_reports.cwe.show 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 stepTitle={ {translate('onboarding.organization.header')} - + } /> 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 } @@ -47,103 +47,8 @@ exports[`works with existing organization 1`] = ` onboarding.organization.header - - -
- } - > -
- - -
- } - > - - -
- } - > - - - - - - - - - - - - - - - - + doc={Promise {}} + /> 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 ( {badge} @@ -121,3 +111,21 @@ const mapStateToProps = (state: any, { organization }: OwnProps) => { }; export default connect(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`] = ` import('./DocMarkdownBlock')); - -interface Props { - className?: string; - path: string; -} - -interface State { - content?: string; -} - -export default class DocInclude extends React.PureComponent { - mounted = false; - state: State = {}; - - componentDidMount() { - this.mounted = true; - this.fetchContent(); - } - - componentWillReceiveProps(nextProps: Props) { - if (nextProps.path !== this.props.path) { - this.setState({ content: undefined }); - } - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.path !== this.props.path) { - this.fetchContent(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchContent = () => { - // 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 ; - } -} 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 { {displayH1 &&

{parsed.frontmatter.title}

} { remark() - // .use(remarkInclude) .use(remarkToc, { maxDepth: 3 }) .use(reactRenderer, { remarkReactComponents: { @@ -69,8 +66,6 @@ export default class DocMarkdownBlock extends React.PureComponent { 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

( return ; }; } - -function filterContent(content: string) { - const beginning = isSonarCloud() ? '' : ''; - const ending = isSonarCloud() ? '' : ''; - - 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 { - 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 ( -

- {this.state.loading ? ( - - ) : ( - - )} -
- ); - } - render() { - return ( + return this.state.content ? ( + overlay={ +
+ +
+ }> {this.props.children}
+ ) : ( + 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(); - wrapper.setState({ content: 'this is *bold* text', open: true, loading: true }); +it('should render', async () => { + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); - wrapper.setState({ loading: false }); + await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); }); - -it('should reset state when receiving new doc', () => { - const wrapper = shallow(); - 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`] = ` - some - +

- sonarqube - +

- long - +

- multiline - +

- text - +

`; @@ -51,25 +51,25 @@ exports[`should cut sonarqube/sonarcloud content 2`] = ` - some - +

- sonarcloud - +

- text - +

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

`; 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`] = ` - - - - } -/> -`; +exports[`should render 1`] = `""`; exports[`should render 2`] = ` ' : ''; + const ending = isSonarCloud() ? '' : ''; + + 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" -- 2.39.5